/* --- Layout widths ------------------------------------------------------------------
 *
 * Two layouts, one breakpoint. Above 900 px the page keeps its fixed desktop
 * width: 1120 px of content, the site menu pulled up beside the search row, the
 * profile and the results floating over the shell. Below 900 px everything that
 * floats comes back into the flow and every multi-column grid folds into one.
 *
 * 900 px rather than 720: a phone in landscape is 844-932 px wide, and at those
 * widths the desktop layout needs horizontal scrolling to be usable at all. The
 * whole mobile block lives at the end of this file - see "Phones and narrow
 * windows" - so it wins by order rather than by specificity, and none of the
 * desktop rules above have to know it exists.
 */
:root {
	--bg: #f4efe6;
	--paper: #fffdf8;
	--ink: #181511;
	--muted: #5c5147;
	--accent: #f26a1b;
	/* The buttons' gradient runs mid -> deep. The accent itself is too light for the
	   buttons' white text (3.1:1), so the gradient starts from the same hue but
	   darker: #c35516 is the brightest 22 degree orange that still passes 4.5:1. The
	   accent is bright in its proper use, as text on black (6.5:1). */
	--accent-mid: #c35516;
	--accent-deep: #a83c0c;
	/* The accent mixed about half and half with the page's cream: a filter that
	   is in the list but was not chosen. Orange enough to belong to the accent's
	   family, pale enough that a chosen filter beside it is unmistakable. */
	--accent-pale: #f3a676;
	/* Vaalea savy tilaviesteille mustalla taustalla. */
	--accent-soft: #ffab84;

	/* --- The back office's own measurements -------------------------------
	 *
	 * The admin is one surface with several pages, and a button that does the
	 * same KIND of thing has to be the same size on all of them. It was not:
	 * MERKITSE KÄSITELLYKSI on the orders page was set to 0.5rem/0.82rem and
	 * TARKISTA NYT on the catalogues page fell back to the shop's own button
	 * (0.72rem/1rem), so the two read as buttons of different importance -
	 * which is a claim about the work, and the wrong one.
	 *
	 * These three are that size. A new admin button uses them rather than a
	 * number of its own; if a button needs to be bigger, the reason belongs in
	 * a comment beside the exception.
	 *
	 * The headings follow the same idea: an eyebrow inside a box opens at
	 * --admin-eyebrow-gap from whatever is above it, and an eyebrow that sits
	 * under a rule of its own opens at nothing - the rule is the space. */
	--admin-button-padding: 0.5rem 1rem;
	--admin-button-size: 0.82rem;
	--admin-button-line: 1.15;
	--admin-eyebrow-gap: 1.6rem;
	/* The search box, the shop's name above it and the bike button below it read
	   as one group even though they sit in three different containers - the
	   header's brand row, its search row and the shell's own first row. This is
	   how far that group stands in from the content column's left edge, and it is
	   one value because moving one of the three without the others would break
	   the group rather than move it. Zero on a narrow screen, where there is no
	   room to stand anything in. */
	--brand-group-shift: 6rem;
	--shadow: 0 20px 60px rgba(16, 8, 4, 0.18);
	--panel-gap: clamp(1.5rem, 3vw, 3rem);
	--panel-width: min(520px, calc((100vw - 6rem - var(--panel-gap)) / 2));
	--results-top: clamp(12rem, 16vw, 14rem);
	--results-width: min(1320px, calc(100vw - 1rem));
	--search-width: min(300px, calc(100vw - 2rem));
	--content-width: min(1120px, 100% - 2rem);
	/* Space under the last action in a view.
	 *
	 * One token rather than a number repeated per view: the cart and the
	 * checkout both end in a button, and a button sitting flush against the
	 * bottom of the box reads as a page that was cut off. Equivalent places
	 * should breathe equally, and that only stays true if there is one value
	 * to change. */
	--action-space: 2.4rem;
	/* The rhythm between elements in the question panel. One value, so the gaps
	   between the title, the ground, the log and the form stay equal - separate
	   margins per block made the panel tight at the top and loose at the
	   bottom. */
	--chat-gap: 1.15rem;
	/* The gap between a bubble and the panel it opens. The same for both bubbles
	   (the customer's chat and the admin's inbox) - they are one component in two
	   roles, and a different gap would look like an accident. */
	--bubble-gap: 1.25rem;
}

* {
	box-sizing: border-box;
}

body {
	margin: 0;
	min-height: 100vh;
	/* Content width 1120px plus margins - but as a maximum the layout grows to,
	   not a floor it refuses to go below.
	
	   It was a floor (min-width: 1180px), and between 900 and 1180 the page
	   simply scrolled sideways: a browser window at 1100 put the cart at 1150,
	   reachable only by scrolling right. The desktop layout reflows perfectly
	   well in that range - the content column is min(1120px, 100% - 2rem)
	   already - so the only thing the floor bought was a scrollbar.
	
	   900 is kept as the floor: below it the mobile block at the end of this file
	   takes over and reflows properly. */
	min-width: 900px;
	font-family: "Space Grotesk", sans-serif;
	color: var(--ink);
	background-color: #000;
	/* The background image covers the screen rather than sitting inside a box. The
	   dark gradient is on top of the image, because the page's text is light and the
	   image's sky is bright.
	   Longhand rather than the background shorthand: the shorthand would reset
	   background-image. */
	background-image:
		linear-gradient(rgba(8, 8, 8, 0.78), rgba(8, 8, 8, 0.9)),
		url("/assets/bg.jpg?v=4");
	background-size: cover;
	background-position: center 42%;
	background-repeat: no-repeat;
	/* Fixed: the image stays put as the page scrolls, so it is the screen's
	   background rather than an image stretching along with the content. */
	background-attachment: fixed;
}

.hidden {
	display: none !important;
}

.bg-grid {
	display: none;
	position: fixed;
	inset: 0;
	background-image:
		linear-gradient(rgba(24, 21, 17, 0.04) 1px, transparent 1px),
		linear-gradient(90deg, rgba(24, 21, 17, 0.04) 1px, transparent 1px);
	background-size: 34px 34px;
	mask-image: radial-gradient(circle at center, black 30%, transparent 80%);
	pointer-events: none;
}

.topbar {
	width: var(--content-width);
	margin: 1rem auto 0;
	display: flex;
	align-items: flex-start;
	justify-content: flex-start;
	position: relative;
	z-index: 3;
}

/* While the search predictions are open, and only then.
 *
 * z-index: 3 makes the bar a stacking context, so the list inside it cannot
 * reach over .finder-cart-row (5) however high its own z-index is - the bike
 * row was drawing on top of the predictions and swallowing the clicks. Lifting
 * the bar permanently would put it over the rows that are meant to be above it,
 * so it is lifted for as long as the list is on the screen. */
.topbar.topbar-ennuste {
	z-index: 30;
}

/* Full width, so the menu can sit at the page's right edge. Without this the
   column narrows to its content and the menu's right edge follows it. */
.topbar-brand {
	width: 100%;
}

.topbar-brand h1 {
	margin: 0;
	color: var(--accent);
	font-size: clamp(2.2rem, 4vw, 3rem);
	line-height: 0.9;
}

.topbar-brand h1 .brand-home-link {
	display: inline-flex;
	align-items: center;
}

/* absolute rather than fixed: the box stays at the top of the page and does not
   follow the scroll. The anchor is .topbar (position: relative), so it aligns to
   the content column's edge rather than the browser window's. */
/* The same position as the profile card, because they are two states of the same
   thing: a signed-out visitor sees the button, a signed-in one the card, and they
   have to swap in place rather than jump.
   The formula is therefore the same as .profile-badge's: the right edge aligned
   with the main box, i.e. half of the remaining space. z-index 5 for the same
   reason as on the card - .topbar is 3 and its box reaches over this one, so at a
   lower value the bar would swallow the clicks. */
.topbar-login-btn {
	position: absolute;
	top: clamp(1rem, 2vw, 1.8rem);
	right: calc((100% - var(--content-width)) / 2);
	padding: 0.55rem 1.2rem;
	font-size: 0.78rem;
	letter-spacing: 0.06em;
	white-space: nowrap;
	/* The same value as the profile box: they are two states of the same place, so
	   they have to sit in the same layer. */
	z-index: 20;
}

/* The form now lives in the dialog, so it no longer positions itself. */
.topbar-login-form {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
	gap: 0.4rem;
	align-items: center;
}

.topbar-login-form input {
	padding: 0.4rem 0.55rem;
	border-radius: 9px;
	font-size: 0.78rem;
}

.topbar-login-form button {
	padding: 0.4rem 0.8rem;
	font-size: 0.75rem;
	white-space: nowrap;
}

.app-shell {
	width: var(--content-width);
	margin: 0 auto 2rem;
	display: flex;
	flex-direction: column;
}

.profile-badge {
	position: absolute;
	top: clamp(1rem, 2vw, 1.8rem);
	/* The right edge aligned with the main box. The box is var(--content-width)
	   wide and centred, so its margin is half of the space that remains. The earlier
	   fixed clamp pulled the profile out of the box's alignment as soon as the window
	   was wider than the box. */
	right: calc((100% - var(--content-width)) / 2);
	width: fit-content;
	/* A min-width alongside fit-content: a short name no longer squeezes the box
	   into a narrow pill, but a long one may still grow it. */
	min-width: 12rem;
	max-width: min(340px, calc(100% - 2rem));
	/* Higher than .finder-cart-row (5). That was why the cart icon and its count
	   showed through the open menu: the same z-index, and the cart's row comes later
	   in the DOM, so it drew on top. The comparison was never about opacity but about
	   paint order.
	   High enough against .topbar (3) as well - without that the bar swallowed the
	   sign-out clicks and there was no way out at all. */
	z-index: 20;
}

.card {
	background: rgba(255, 253, 248, 0.7);
	border: 1px solid rgba(24, 21, 17, 0.1);
	border-radius: 20px;
	box-shadow: var(--shadow);
	padding: clamp(1rem, 2vw, 2rem);
}

.card.profile-badge {
	/* The same glass surface as the main box: a very transparent white. It requires
	   light text, because the body's --ink is dark and no longer reads against the
	   black showing through. */
	border: 1px solid rgba(255, 255, 255, 0.18);
	background: rgba(255, 255, 255, 0.06);
	box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
	backdrop-filter: blur(8px);
	color: #f4efe6;
	padding: 0.7rem 1rem;
	/* A row: the name to the left, sign-out at the right edge. The button appears
	   only when the garage is empty, so with bikes the row holds only the name. */
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1rem;
	/* Do NOT put position: relative here. .profile-badge is already position:
	   absolute, and this selector is more specific (0,2,0 against 0,1,0) - relative
	   overrode it, the box dropped into normal flow between the heading and the
	   content, and with it the sign-out went out of reach for a signed-in user.
	   Absolute is enough as the menu's anchor: it is just as much a positioned
	   element, so a position: absolute child aligns to this. */
}

/* The name and the caret: the whole closed box is one button. */
.profile-badge .profile-badge-toggle {
	/* flex: 1 so the button fills the box's row. Without it the button shrinks to
	   its content and the arrow ends up beside the name in the middle of the box
	   rather than at its right edge. */
	flex: 1;
	min-width: 0;
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	grid-template-areas: "label caret" "name caret";
	align-items: center;
	gap: 0 0.6rem;
	margin: 0;
	padding: 0;
	background: none;
	border: 0;
	border-radius: 8px;
	color: inherit;
	font: inherit;
	text-align: left;
	cursor: pointer;
}

.profile-badge .profile-badge-toggle:hover {
	background: none;
}

.profile-badge-toggle .eyebrow {
	grid-area: label;
}

.profile-badge-name {
	grid-area: name;
	display: block;
	min-width: 0;
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.15rem;
	letter-spacing: 0.03em;
	overflow-wrap: anywhere;
}

/* The caret says there is something to open here. It turns in the open state, so
   it also says which way things are going. */
.profile-badge-caret {
	grid-area: caret;
	width: 0.7rem;
	height: 0.5rem;
	color: var(--accent);
	transition: transform 140ms ease;
}

/* The menu: the garage and sign-out.
   top: 100% lands on the bottom of the box's padding box, so no gap is left
   between the menu and the box - a gap would break the hover the moment the mouse
   tries to move into the menu. */
/* visibility and opacity rather than display: none, because display cannot be
   delayed and a transition has no effect on it.
   The delay is the whole point: the menu opens only once the mouse has been over
   the box for 240 ms. Without it, it opened every time the mouse passed by on its
   way to the cart, and covered the cart just as it was about to be clicked.
   Closing has no delay, so it disappears the moment the mouse leaves. */
.profile-badge-menu {
	visibility: hidden;
	opacity: 0;
	transition: opacity 120ms ease, visibility 120ms;
	/* The closing delay. This is the base state's delay, and the base state takes
	   effect when the hover is lost - so this is how long the menu waits before
	   closing. Without it the menu vanished mid-movement while the mouse was on its
	   way to the KIRJAUDU ULOS button. */
	transition-delay: 420ms;
	position: absolute;
	top: 100%;
	/* A few pixels past the box's right edge: at exactly 0 the menu looks slightly
	   too far left, because the box's shadow widens its visual edge. */
	right: -0.35rem;
	min-width: 16.5rem;
	max-width: min(21rem, calc(100vw - 2rem));
	/* A small gap to the box, so the two read as two boxes.
	   The gap was removed for a while, because it broke the hover mid-movement and
	   required a fragile ::before bridge. It is now safe without one: the 420 ms
	   closing delay covers the crossing, so a momentary loss of hover does not have
	   time to close the menu. */
	margin-top: 0.45rem;
	padding: 1.2rem 1.3rem;
	border: 1px solid rgba(255, 255, 255, 0.18);
	border-radius: 14px;
	/* Fully opaque. At 0.96 it let 4 % through, and the bright orange cart badge
	   showed in it - a backdrop-filter does not help when the problem is opacity.
	   The filter was removed, because behind a fully opaque surface it does
	   nothing. */
	background: #1a1a1a;
	box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
}

/* Two ways of opening it, one in CSS and one in JS:
          :hover   the mouse - closes as soon as the mouse leaves
          a class  click, touch and keyboard, set by JS
        :focus-within was REMOVED. It was the problem rather than the solution: a click
        leaves the focus on the button, so the menu stayed open even when the class was
        removed - and clicking again could not close it. A menu left open covered the
        content beneath it, e.g. the cart icon, which could then not be clicked. The
        keyboard is now handled in JS with a :focus-visible check, which is false after
        a mouse click.
        margin-top: 0.4rem leaves a gap, so the hover bridge is built with a ::before
        element that covers it. Without that the menu would disappear as the mouse
        crosses the gap. */
/* The caret and the menu only when the garage holds something. On an empty
   garage the caret would promise a block that is not there, and that is worse
   than no caret. */
.profile-badge-caret {
	display: none;
}

.profile-badge-has-bikes .profile-badge-caret {
	display: block;
}

.profile-badge-has-bikes:hover .profile-badge-menu {
	visibility: visible;
	opacity: 1;
	transition-delay: 240ms;
}

/* A click and the keyboard open it at once: by then the user has already stated
   their intent and no delay is needed. */
.profile-badge-has-bikes.profile-badge-open .profile-badge-menu {
	visibility: visible;
	opacity: 1;
	transition-delay: 0ms;
}

/* Sign-out on the box's row only with an empty garage; with bikes it lives
   inside the menu. */
.profile-badge-has-bikes .profile-logout-inline {
	display: none;
}

.profile-badge .profile-logout-inline {
	margin: 0;
	padding: 0.4rem 0.8rem;
	font-size: 0.72rem;
	letter-spacing: 0.05em;
	white-space: nowrap;
	flex: none;
}

/* A button is not a button with nothing to open: the cursor says so before the
   click. */
.profile-badge:not(.profile-badge-has-bikes) .profile-badge-toggle {
	cursor: default;
}

.profile-badge-has-bikes:hover .profile-badge-caret {
	transform: rotate(180deg);
	transition-delay: 240ms;
}

.profile-badge.profile-badge-open .profile-badge-caret {
	transform: rotate(180deg);
	transition-delay: 0ms;
}

@media (prefers-reduced-motion: reduce) {
	.profile-badge-caret {
		transition: none;
	}
	/* The movement goes, but the DELAY stays: it is not an animation but a reading of
	   intent, and without it the menu would again open when the mouse merely passes
	   by. */
	.profile-badge-menu {
		transition: visibility 0s;
	}
}

.profile-badge-logout {
	margin-top: 2.4rem;
	width: 100%;
	/* A touch heavier than the base size (0.72rem): it is the menu's only action and
	   spans its whole width.
	   The font size is stated here because .profile-badge > button shrinks it to
	   0.75rem - that rule no longer matches this button, but the value is written
	   down so it does not depend on it. */
	padding: 0.85rem 1.2rem;
	font-size: 0.8rem;
}

/* The garage is the menu's first block, so it needs no rule above it.
   No rule below it either: that was tried, but a gap alone is enough to separate
   sign-out from the garage - the line was one mark too many in a three row
   menu. */
.profile-badge-menu .garage-section {
	margin-top: 0;
	padding-top: 0;
	border-top: 0;
}

.profile-badge h2 {
	font-size: 1.05rem;
	margin: 0;
	line-height: 1.1;
	/* The name comes from the data, so a long one is cut with an ellipsis rather
	   than let past the box's edge. */
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	min-width: 0;
}

.profile-badge .ghost {
	flex: 0 0 auto;
}

.profile-badge .description {
	font-size: 0.7rem;
	margin: 0;
}

/* DIRECT children only. Without the > this also matched the sign-out button
   inside the menu, and specificity 0,1,1 beat .profile-badge-logout's 0,1,0 - so
   making that button bolder had no visible effect at all. This rule is for the
   box row's small buttons, and the menu is not that row. */
.profile-badge > button {
	margin: 0;
	padding: 0.32rem 0.85rem;
	font-size: 0.75rem;
	white-space: nowrap;
}

h1,
h2 {
	margin: 0;
	font-family: "Bebas Neue", sans-serif;
	letter-spacing: 0.04em;
	line-height: 1;
}

h1 {
	font-size: clamp(2rem, 5vw, 3.4rem);
}

h2 {
	font-size: clamp(1.8rem, 4vw, 2.6rem);
}

.eyebrow {
	margin: 0;
	text-transform: uppercase;
	letter-spacing: 0.16em;
	color: var(--accent);
	font-weight: 700;
	font-size: 0.75rem;
}

/* The back office's box headings in the shop's heading face, and at a size that
   reads as a heading rather than as a label. The shop's own sections are named
   in Bebas throughout - the breadcrumb, the section titles, the menu - and the
   admin is the same shop seen from the other side. */
.admin-panel .eyebrow {
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.05rem;
	font-weight: 400;
	letter-spacing: 0.08em;
}

.description {
	margin: 0.5rem 0 0;
	color: var(--muted);
}

input {
	width: 100%;
	color: var(--ink);
	border: 1px solid rgba(24, 21, 17, 0.18);
	background: rgba(255, 255, 255, 0.92);
	border-radius: 12px;
	padding: 0.7rem 0.8rem;
	font: inherit;
}

button {
	appearance: none;
	border: 0;
	border-radius: 999px;
	background: linear-gradient(140deg, var(--accent-mid), var(--accent-deep));
	color: #fff;
	font: inherit;
	font-weight: 700;
	padding: 0.72rem 1rem;
	cursor: pointer;
}

button.ghost {
	background: linear-gradient(140deg, var(--accent-mid), var(--accent-deep));
}

.profile-card {
	position: fixed;
	top: clamp(10.5rem, 15vw, 13rem);
	left: calc(50% - (var(--panel-gap) / 2) - var(--panel-width));
	width: var(--panel-width);
	max-height: calc(100vh - 15rem);
	overflow-y: auto;
	z-index: 2;
	backdrop-filter: blur(8px);
	background: transparent;
	border: 0;
	box-shadow: none;
	padding: 0;
	width: 0;
	height: 0;
	overflow: visible;
}

.results-card {
	position: fixed;
	top: var(--results-top);
	left: 50%;
	transform: translateX(-50%);
	width: var(--results-width);
	max-height: calc(100vh - 15rem);
	overflow-y: auto;
	z-index: 2;
	backdrop-filter: blur(10px);
	background: rgba(255, 253, 248, 0.58);
	border: 1px solid rgba(255, 255, 255, 0.34);
	padding: clamp(0.85rem, 1.6vw, 1.2rem);
}

.search-form,
.results-head-row {
	margin-top: 0.9rem;
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto auto;
	gap: 0.5rem;
	align-items: center;
}

/* ...and everything the row actually holds is pressable as before. */
.search-row > * {
	pointer-events: auto;
}

#searchForm {
	/* The margin is on the search row: the form is no longer the row's only
	   content. */
	margin-top: 0;
	/* The prediction list hangs off the form, so it opens against the box
	   itself - on a phone the row wraps and is much taller than the box. */
	position: relative;
	/* Stretches into whatever room the menu leaves. A fixed width would mean
	   guessing the menu's width, and a guess either shortens the field for nothing or
	   pushes the menu onto a second row - the browser measures it exactly. */
	flex: 1 1 var(--search-width);
	width: auto;
	max-width: min(460px, 100%);
	padding: 0.45rem 0.75rem;
	border-radius: 14px;
	border: 1px solid rgba(255, 255, 255, 0.18);
	background: rgba(255, 255, 255, 0.06);
	grid-template-columns: minmax(0, 1fr) auto;
}

body.admin-mode #searchForm {
	display: none !important;
}

.profile-card > .eyebrow,
#sectionTitle {
	display: none;
}

.garage-section {
	margin-top: 1.1rem;
	padding-top: 1rem;
	border-top: 1px solid rgba(255, 255, 255, 0.16);
}

/* A vertical list rather than a row of chips: the profile box is narrow (max
   340px), and horizontal chips would wrap onto several lines there anyway. A
   vertical list is more readable and its rows are the same length. */
.garage-list {
	margin: 0.9rem 0 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
}

.menu-list {
	margin: 1rem auto 0;
	padding: 0 0.55rem 0.2rem;
	list-style: none;
	display: flex;
	flex-wrap: wrap;
	justify-content: flex-start;
	align-items: center;
	gap: 0.3rem;
	width: max-content;
	max-width: 100%;
	white-space: nowrap;
}

.menu-list > li {
	position: relative;
	flex: 0 0 auto;
}

/* Three columns: icon, name, delete. Grid rather than flex, so the bin is always
   at the right edge regardless of the name's length. */
.garage-item {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr) auto;
	align-items: center;
	gap: 0.6rem;
	padding: 0.6rem 0.7rem;
	border: 1px solid rgba(255, 255, 255, 0.18);
	border-radius: 10px;
	background: rgba(255, 122, 24, 0.12);
}

/* The garage row's text in the profile box.
   .garage-menu-link is var(--ink), i.e. nearly black, which worked as long as the
   garage lived on the light shop card. The profile box is a dark glass surface, so
   the move made the link dark on dark. Measured against the garage row's surface,
   rgb(76,59,50):

        var(--ink)      1.71:1   <- effectively invisible
        #f4efe6         9.27:1
        --accent-soft   5.77:1   (hover) */
/* The delete button is smaller in the profile box: a 2rem pill takes an
   unreasonable share of a narrow 340px box, and the row is only one line high. */
.profile-badge .garage-remove {
	width: 1.6rem;
	height: 1.6rem;
}

.profile-badge .garage-menu-link,
.profile-badge .garage-bike {
	color: #f4efe6;
}

.profile-badge .garage-menu-link:hover,
.profile-badge .garage-menu-link:focus-visible {
	color: var(--accent-soft);
}

/* Ikoni on musta, kuten pyoravalitsimen sirussa, ja kaannetaan vaaleaksi
   samalla suodattimella - profiililaatikko on tumma lasipinta. */
.garage-icon {
	display: block;
	width: 1.1rem;
	height: 1.1rem;
	filter: brightness(0) invert(1);
	opacity: 0.85;
}

.garage-menu-link,
.menu-link {
	color: var(--ink);
	font-weight: 700;
	text-decoration: none;
}

.menu-link {
	display: block;
	padding: 0.38rem 0.58rem;
	border-radius: 10px;
	border: 1px solid rgba(24, 21, 17, 0.2);
	background: rgba(255, 255, 255, 0.62);
	white-space: nowrap;
	font-size: 0.9rem;
	/* The same hover as in the front page's large menu (.front-menu): this view holds
	   two chapter menus at once - the front page's large names and this row of pills -
	   and they behaved differently. The pills had no hover rule at all, so hovering
	   them did nothing.

	   The growth is by transform rather than font-size: a font size change would
	   re-lay the row, and this row wraps - the neighbours would jump and the hovered
	   pill could escape from under the cursor.

	   position + z-index, so a grown pill draws over its neighbours. The li has
	   position: relative but no z-index, so it creates no stacking context of its own
	   and this z-index compares against all siblings. The submenu popup is 6, so it
	   stays above this too. */
	position: relative;
	z-index: 0;
	transition: transform 0.14s ease, color 0.12s ease, border-color 0.12s ease;
}

/* Muut pienenevat kun valikkoa hoveroidaan... */
.menu-list:hover > li > .menu-link {
	transform: scale(0.9);
}

/* ...and the one under the cursor grows. The same specificity as the rule above
   (0,3,1), so it wins by order - this one comes later. */
.menu-list > li:hover > .menu-link {
	transform: scale(1.18);
	z-index: 2;
}

/* Hoverattu teksti korostusvarilla. */
.menu-list > li:hover > .menu-link {
	color: var(--accent);
	border-color: rgba(191, 58, 31, 0.45);
}

@media (prefers-reduced-motion: reduce) {
	.menu-link {
		transition: color 0.12s ease, border-color 0.12s ease;
	}
}

.shop-empty-box {
	position: fixed;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	width: min(760px, calc(100vw - 2rem));
	padding: 1.5rem 1.8rem;
	border-radius: 14px;
	border: 1px solid rgba(24, 21, 17, 0.12);
	background: rgba(255, 255, 255, 0.94);
	box-shadow: 0 16px 40px rgba(16, 8, 4, 0.18);
	text-align: center;
	font-weight: 700;
	font-size: clamp(1.15rem, 2vw, 1.6rem);
	letter-spacing: 0.03em;
	color: #1c1814;
	z-index: 5;
}

body.admin-mode .shop-empty-box {
	display: none !important;
}

/* The shop view is hidden in admin mode. The site menu belongs with it: nobody
   browses the shop on the admin page, so the menu would be a leftover from the
   shop interface. */
body.admin-mode .glass-box,
body.admin-mode .site-nav,
body.admin-mode .finder-cart-row {
	display: none !important;
}

/* The chat bubble belongs to the customer, not the admin: they do not ask
   themselves. The bubble is inside .glass-box so it disappears with it, but it is
   still said explicitly - if the bubble ever moves outside the box, it must not
   be left hanging in the admin view. */
body.admin-mode .chat-widget {
	display: none !important;
}

body.shop-empty .profile-card,
body.shop-empty .results-card {
	display: none !important;
}

/* The selected chapter: orange text and an underline, and it stays that way
   regardless of what is hovered.
   
   It used to be an orange gradient with white text. Now the hover orange and the
   selection orange are the same colour, and the underline is what separates a
   lasting selection from a passing hover - two different oranges would speak of two
   different things. The same reasoning as .front-category-active.

   All three selectors (0,1,0)...(0,3,1) and AFTER the rules above, so a hover
   wipes out neither the colour nor the underline. */
.menu-link-active,
.menu-list:hover > li > .menu-link-active,
.menu-list > li:hover > .menu-link-active {
	color: var(--accent);
	border-color: rgba(191, 58, 31, 0.45);
	background: rgba(255, 255, 255, 0.62);
	text-decoration: underline;
	text-decoration-thickness: 2px;
	text-underline-offset: 3px;
}

/* The selected one does not shrink with the rest: it is the row's anchor. */
.menu-list:hover > li > .menu-link-active {
	transform: none;
}

.results-label {
	margin: 0.1rem 0 0;
}

.results-model {
	color: var(--accent);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	font-size: clamp(1.4rem, 2.2vw, 2rem);
}

.garage-remove {
	display: inline-grid;
	place-items: center;
	width: 2rem;
	height: 2rem;
	border-radius: 999px;
	background: linear-gradient(140deg, var(--accent-mid), var(--accent-deep));
	color: #fff;
	text-decoration: none;
}

.trash-icon {
	width: 0.95rem;
	height: 0.95rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.8;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.clear-results-btn {
	background: transparent;
	border: 1px solid rgba(24, 21, 17, 0.2);
	color: var(--muted);
	font-size: 1.5rem;
	line-height: 1;
	padding: 0.2rem 0.65rem;
	border-radius: 999px;
	cursor: pointer;
	flex-shrink: 0;
}

.clear-results-btn:hover {
	background: rgba(191, 58, 31, 0.08);
	color: var(--accent);
	border-color: var(--accent);
}

.product-grid {
	margin-top: 1rem;
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: 0.75rem;
}

.product-item {
	display: flex;
	flex-direction: column;
	gap: 0.4rem;
}

.product-img {
	width: 100%;
	aspect-ratio: 4 / 3;
	object-fit: cover;
	border-radius: 10px;
	border: 1px solid rgba(24, 21, 17, 0.1);
}

.product-title {
	margin: 0;
	font-family: "Bebas Neue", sans-serif;
	letter-spacing: 0.04em;
	font-size: 1rem;
	color: var(--ink);
}

.product-meta {
	margin: 0;
	font-size: 0.92rem;
	color: var(--muted);
}

.product-price {
	margin: 0;
	font-family: "Bebas Neue", sans-serif;
	letter-spacing: 0.05em;
	font-size: 1.1rem;
	color: var(--accent);
}

/* Transparent positioning wrapper only - the visible panel is the <ul> inside it. The wrapper
   sits flush against its parent (top: 100%) and uses padding as the visual gap, so the space
   between a link and its submenu is still part of the hover area. With a real gap the popup
   closes while the mouse crosses it and can never be reached. */
/* Drops down over the results area below. The menu sits at the top of .results-card, which
   scrolls (overflow-y: auto) and clips anything past its edges - opening downward from the top
   keeps the panel inside the card, so it stays visible without giving up that scrolling. */
.inline-submenu-popup {
	display: none;
	position: absolute;
	top: 100%;
	left: 0;
	padding-top: 0.35rem;
	min-width: 220px;
	max-width: min(70vw, 420px);
	z-index: 6;
}

/* Third level flies out to the side instead of stacking, with the bridge padding on the left
   for the same reason. */
.inline-submenu-item > .inline-submenu-popup {
	top: 0;
	left: 100%;
	padding-top: 0;
	padding-left: 0.35rem;
}

/* The whole menu tree is rendered once and stays in the DOM - each level's popup only opens
   on hover of that specific item, and hovering the open popup itself keeps it open too so the
   mouse can travel from the link into its own submenu without it closing. */
.menu-list > li:hover > .inline-submenu-popup,
.inline-submenu-item:hover > .inline-submenu-popup {
	display: block;
}

.inline-submenu {
	margin: 0;
	padding: 0.45rem;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: 0.15rem;
	border-radius: 12px;
	border: 1px solid rgba(24, 21, 17, 0.16);
	background: rgba(255, 253, 248, 0.96);
	box-shadow: 0 12px 28px rgba(16, 8, 4, 0.15);
}

.inline-submenu-item {
	position: relative;
	margin-left: 0;
}

/* Block-level so the whole row is a hover and click target, not just the glyphs - a wider
   target makes the cascade far easier to keep open while moving the mouse. */
.inline-submenu-link {
	display: block;
	padding: 0.3rem 0.5rem;
	border-radius: 8px;
	color: var(--accent);
	text-decoration: none;
	font-weight: 600;
	font-size: 0.92rem;
	white-space: nowrap;
}

.inline-submenu-item:hover > .inline-submenu-link {
	background: rgba(191, 58, 31, 0.1);
}

.inline-submenu-link-active {
	color: #fff;
	background: linear-gradient(140deg, var(--accent-mid), var(--accent-deep));
}

.inline-submenu-item:hover > .inline-submenu-link-active {
	color: #fff;
	background: linear-gradient(140deg, var(--accent-mid), var(--accent-deep));
}

/* --- Loyda oma pyorasi ------------------------------------------------------------ */
/*
   Below the search field and aligned with it. The icon is the same as in the
   browser tab, so the button reads as part of the same shop.
   */
.bike-finder-btn {
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	padding: 0.5rem 1.1rem;
	/* Inherited Space Grotesk as on the other buttons. Bebas Neue is an all-caps face
	   whose umlaut coverage cannot be relied on: the browser substituted the missing
	   glyphs from another font, and it showed as blurriness in exactly the ä and the
	   ö. */
	font-size: 1rem;
	letter-spacing: 0.01em;
	white-space: nowrap;
}

.bike-finder-icon {
	display: block;
	width: 1.35rem;
	height: 1.35rem;
	/* The icon is black; the button's gradient is a dark orange, so it is inverted
	   to light in order to stand out. */
	filter: brightness(0) invert(1);
}

.bike-finder {
	/* Floats below the button: a list in the flow would push both the menu and the
	   box. */
	position: absolute;
	top: 100%;
	left: 0;
	/* Above the cart, whose link is z-index 4 in this same row. The panel opens
	   downwards over the place the cart sits on a phone, and without this the
	   cart painted on top of it - exactly over the panel's close button, so the
	   window could not be closed at all. Closing the panel brings the cart
	   back, because it is only ever covered. */
	z-index: 6;
	margin: 0.5rem 0 0;
	/* The width comes from the longest item: every menu gets the same room that a
	   model family needs ("Adventure Touring (281)", 23 characters). The page's
	   content area is 1120 px, so this is nearly as wide. */
	width: min(1030px, 92vw);
	max-height: 40vh;
	overflow-y: auto;
	/* The scrollbar in the shop's orange, and a shadow at the edge that is still
	   scrollable.
	 *
	 * Both, because neither is enough on its own: iOS draws overlay scrollbars
	 * that appear only while a finger is moving, so there the bar says nothing
	 * about whether the list continues. The shadow is the classic two-layer
	 * background trick - a cover layer that scrolls WITH the content (local) and
	 * a shadow layer that does not (scroll), so the shadow is hidden by the
	 * cover exactly when that end has been reached. */
	scrollbar-width: thin;
	scrollbar-color: var(--accent) rgba(255, 255, 255, 0.12);
	padding: 1.5rem 1.4rem 2.6rem;
	border-radius: 12px;
	/* A solid grey: it stands out from the dark page, and through a translucent one
	   the content underneath would show. The tint leans slightly warm rather than
	   neutral, so it sits with the shop's orange. */
	background:
		linear-gradient(#1c1a19 30%, rgba(28, 26, 25, 0)) top / 100% 26px no-repeat local,
		linear-gradient(rgba(28, 26, 25, 0), #1c1a19 70%) bottom / 100% 26px no-repeat local,
		radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0))
			top / 100% 14px no-repeat scroll,
		radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0))
			bottom / 100% 14px no-repeat scroll,
		#1c1a19;
	border: 1px solid rgba(255, 255, 255, 0.22);
	box-shadow: 0 14px 34px rgba(0, 0, 0, 0.55);
	display: grid;
	gap: 0.55rem;
}

.bike-finder::-webkit-scrollbar {
	width: 10px;
}

.bike-finder::-webkit-scrollbar-track {
	background: rgba(255, 255, 255, 0.10);
	border-radius: 999px;
}

.bike-finder::-webkit-scrollbar-thumb {
	background: var(--accent);
	border-radius: 999px;
	border: 2px solid #1c1a19;
}

/* The selected bike in the button's place. The same height and shape as the
   button, so the row does not jump as the filter is switched on or off. */
.bike-chip {
	display: inline-flex;
	align-items: stretch;
	border-radius: 999px;
	border: 1px solid var(--accent-mid);
	background: rgba(255, 122, 24, 0.14);
	overflow: hidden;
}

.bike-chip-open,
.bike-chip-clear {
	background: none;
	border: 0;
	border-radius: 0;
	color: #f4efe6;
	font: inherit;
	cursor: pointer;
}

.bike-chip-open {
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	padding: 0.5rem 0.9rem;
	font-size: 0.92rem;
}

.bike-chip-name {
	font-weight: 700;
	letter-spacing: 0.02em;
}

.bike-chip-meta {
	font-size: 0.8rem;
	opacity: 0.75;
	letter-spacing: 0.06em;
}

/* Flex centring rather than tuned padding. The chip has align-items: stretch, so
   this button takes the chip's height - and at that height where the × lands is
   at the mercy of the browser's own button metrics. The HAE button had the same
   fault. So the button is its own centring container, and the line height comes
   from the chip's height. */
.bike-chip-clear {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	padding: 0 0.75rem;
	font-size: 1.3rem;
	line-height: 1;
	border-left: 1px solid rgba(255, 122, 24, 0.5);
}

.bike-chip-open:hover,
.bike-chip-clear:hover,
.bike-chip-open:focus-visible,
.bike-chip-clear:focus-visible {
	background: rgba(255, 122, 24, 0.28);
}

/* The universal button beside the bike chip. It says whether universal parts are
   included in the scoped shop, and a click toggles the state.

   Two different looks deliberately: swapping the text alone would leave the button
   looking the same either way, and the user would not see at a glance whether the
   filter is on. On, it is highlighted like the bike chip; off, dim and dashed. */
.universal-chip {
	display: inline-flex;
	align-items: center;
	padding: 0.5rem 0.9rem;
	border-radius: 999px;
	background: rgba(255, 122, 24, 0.14);
	border: 1px solid var(--accent-mid);
	color: #f4efe6;
	font: inherit;
	font-size: 0.82rem;
	font-weight: 700;
	letter-spacing: 0.05em;
	cursor: pointer;
}

.universal-chip[aria-pressed="false"] {
	background: none;
	border-style: dashed;
	border-color: rgba(255, 255, 255, 0.32);
	opacity: 0.72;
	font-weight: 400;
}

.universal-chip:hover,
.universal-chip:focus-visible {
	background: rgba(255, 122, 24, 0.28);
	opacity: 1;
}

/* The × in the box's top right corner. The general button rule gives buttons the
   orange gradient, so the background and border are reset here. */
.bike-finder-close {
	position: absolute;
	top: 0.55rem;
	right: 0.7rem;
	background: none;
	border: 0;
	padding: 0.1rem 0.45rem 0.25rem;
	border-radius: 8px;
	color: #f4efe6;
	opacity: 0.6;
	font-size: 1.5rem;
	line-height: 1;
	font-weight: 400;
	cursor: pointer;
}

.bike-finder-close:hover,
.bike-finder-close:focus-visible {
	opacity: 1;
	background: rgba(255, 255, 255, 0.12);
}

.bike-finder-title {
	/* Inherited Space Grotesk. The heading used to be in Bebas Neue, because "OMA
	   MOPO" contained no Finnish umlauts - "OMA PYÖRÄ" does, and Bebas Neue is an
	   all-caps face whose umlaut coverage cannot be relied on. The browser
	   substituted the missing glyphs from another font, and it showed as blurriness
	   in exactly the ä and the ö (the same fault as in .bike-finder-btn earlier). The
	   all-caps effect now comes from letter-spacing and weight. */
	font-size: 1.5rem;
	font-weight: 700;
	letter-spacing: 0.1em;
	margin: 0 0 1.5rem;
	color: #f4efe6;
}

/* The selects and VALITSE on one row. The labels live inside each select as its
   first option (as in the supplier's own picker) and an aria-label says the same
   to a screen reader. */
.bike-finder-row {
	display: flex;
	align-items: center;
	gap: 0.5rem;
}

/* Every select the same length. The model family's longest label (23 characters,
   "Adventure Touring (281)") does not fit when closed and is cut with an
   ellipsis; the open list shows it in full. */
.bike-finder-row .bike-finder-select {
	flex: 1 1 0;
	min-width: 0;
}

.bike-finder-row button {
	flex: 0 0 auto;
	padding: 0.62rem 1.3rem;
	font-size: 0.9rem;
}

.bike-finder-note {
	margin-top: 2rem;
}

.bike-finder-select {
	appearance: none;
	-webkit-appearance: none;
	width: 100%;
	padding: 0.62rem 1.9rem 0.62rem 0.7rem;
	border-radius: 8px;
	border: 1px solid rgba(255, 255, 255, 0.35);
	background-color: #fff;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%23111' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
	background-repeat: no-repeat;
	background-position: right 0.7rem center;
	background-size: 0.62rem auto;
	color: #111;
	font: inherit;
	font-size: 0.95rem;
	text-overflow: ellipsis;
}

.bike-finder-select option {
	background-color: #fff;
	color: #111;
}

.bike-finder-select:disabled {
	opacity: 0.45;
}

/* Dim: the link is a placeholder and does not lead anywhere yet. */
.bike-finder-traficom {
	font-size: 1rem;
	color: #f4efe6;
	/* Dim but readable: the opacity is set relative to the box's background. */
	opacity: 0.55;
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

/* The instruction below the link rather than in its place: the link says where
   and the instruction says what happens there. Smaller and dimmer than the link so
   it does not compete with it - the instruction is read once, the link is
   pressed. */
.bike-finder-traficom-info {
	margin: 0.3rem 0 0;
	max-width: 34rem;
	color: #f4efe6;
	opacity: 0.42;
	font-size: 0.82rem;
	line-height: 1.4;
}


/* The prediction list, under the search box.
 *
 * Absolute rather than in the flow: it opens while someone is typing and closes
 * as soon as they stop, and in the flow that moved the whole page down and back
 * up under their hands. It is anchored to the row rather than to the form, so
 * the list is as wide as the box even when the site menu sits beside it. */
.search-suggestion {
	position: absolute;
	top: 100%;
	left: 0;
	/* At least as wide as the box, and wider where there is room: a section's
	   name is long ("Kytkin, 1998-2000 Evolution Big Twin ja ..."), and a list
	   the width of the box would cut every one of them at the same place. */
	min-width: 100%;
	width: max-content;
	max-width: min(560px, calc(100vw - 2rem));
	margin-top: 0.35rem;
	z-index: 40;
	display: grid;
	/* The rows are as wide as the box and no wider.
	 *
	 * Without this the column is sized from the longest row's content - a
	 * section name can be seventy characters - and on a phone the rows ran 659
	 * pixels wide inside a 358 pixel box: the name was cut by the screen's edge
	 * and the count beside it was simply off it. The names have their own
	 * ellipsis (.suggestion-name), which only works once the row is told how
	 * much room it has. */
	grid-template-columns: minmax(0, 1fr);
	gap: 0.15rem;
	padding: 0.4rem;
	border-radius: 12px;
	border: 1px solid rgba(24, 21, 17, 0.14);
	background: rgba(255, 255, 255, 0.98);
	box-shadow: 0 12px 28px rgba(24, 21, 17, 0.14);
	max-height: min(60vh, 26rem);
	overflow-y: auto;
}

/* Which of the two halves of a search sentence this row is. A label rather
 * than a heading: it names the group and gets out of the way. */
.suggestion-label {
	margin: 0.3rem 0 0.1rem;
	padding: 0 0.5rem;
	font-family: "Space Grotesk", sans-serif;
	font-size: 0.68rem;
	letter-spacing: 0.08em;
	color: rgba(24, 21, 17, 0.5);
}

.suggestion-label:first-child {
	margin-top: 0;
}

.suggestion-btn {
	width: 100%;
	display: flex;
	align-items: baseline;
	gap: 0.5rem;
	border-radius: 8px;
	border: 0;
	background: transparent;
	color: var(--ink);
	font-weight: 500;
	font-size: 0.95rem;
	text-align: left;
	padding: 0.45rem 0.5rem;
	cursor: pointer;
}

.suggestion-name {
	flex: 1 1 auto;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* The family and the years for a motorcycle, the chapter for a section: what
 * tells two rows with similar names apart. */
.suggestion-hint {
	flex: 0 0 auto;
	font-size: 0.78rem;
	color: rgba(24, 21, 17, 0.55);
}

/* The section's own number, the same one the menu shows - the count is a
 * promise about what pressing the row gives. */
.suggestion-count {
	flex: 0 0 auto;
	font-family: "Space Grotesk", sans-serif;
	font-size: 0.78rem;
	font-variant-numeric: tabular-nums;
	color: rgba(24, 21, 17, 0.55);
}

.suggestion-btn:hover,
.suggestion-btn.suggestion-active {
	background: rgba(255, 240, 220, 0.95);
}

.status {
	min-height: 1.4rem;
	margin: 0.5rem auto 0;
	width: min(900px, 100% - 2rem);
	color: var(--accent-soft);
	font-weight: 500;
	font-family: "Space Grotesk", sans-serif;
	font-size: 0.9rem;
}

.status.ok {
	color: #8fd98a;
}

.admin-panel {
	max-width: 760px;
	margin: 0 auto;
	backdrop-filter: blur(8px);
	/* The same opacity as the shop's main box: the background image shows through but
	   does not take attention. .card gives a light cream, so it is overridden here -
	   and with it the text and field colours, because light text on a light ground was
	   what made .card unreadable. */
	background-color: rgba(10, 10, 10, 0.55);
	border-color: rgba(255, 255, 255, 0.18);
	color: #f4efe6;
}

/* The catalogue name is the first thing in the box, so the box's own top
   padding was stacking on top of a heading that needs no runway. */
.admin-panel .admin-main-box {
	padding-top: 0.9rem;
}

/* The inner boxes a shade lighter, so they stand out from the panel. */
.admin-panel .admin-box {
	background-color: rgba(255, 255, 255, 0.05);
	border-color: rgba(255, 255, 255, 0.12);
	color: #f4efe6;
}

.admin-panel .description {
	color: rgba(244, 239, 230, 0.75);
}

/* The fields stay light with dark text: a white field on a dark ground is where
   the user types, and it has to stand out. */
.admin-panel .admin-file-text,
.admin-panel textarea.admin-file-text {
	background: rgba(255, 255, 255, 0.95);
	color: var(--ink);
	border: 1px solid rgba(255, 255, 255, 0.3);
	border-radius: 10px;
	padding: 0.55rem 0.7rem;
	font: inherit;
	width: 100%;
	box-sizing: border-box;
}

.admin-grid {
	display: grid;
	grid-template-columns: 2.2fr 1fr;
	gap: 0.9rem;
	align-items: start;
}

/* One left-aligned column for the single-box sections. The product form is the
   main thing on its own view, so it gets its own width and more room - in a
   2.2fr/1fr grid it sat in the narrow column and the fields were cramped.
   Left rather than centred, so it lines up with the KATALOGI box, which starts
   at the same edge; a centred box shifted sideways when switching sections. */
.admin-grid-single {
	grid-template-columns: minmax(0, 34rem);
	justify-content: start;
}

/* The edit section: field form on the left, the manual-edit list on the right.
   .admin-panel prefix for the same reason as the forms below - .admin-grid is
   redefined later in this file at the same specificity. */
.admin-panel .admin-grid-edit {
	/* The list first and wider: it is what the page is for - what has been done
	   to this catalogue by hand - and each of its rows carries a picture and a
	   story. The search beside it is the same width as the orders page's own
	   side box (21rem), so the two pages read as the same room: a list with
	   something narrow beside it. */
	grid-template-columns: minmax(0, 1fr) minmax(0, 21rem);
	gap: 0.9rem;
	/* start: the list box ends where its content ends. Stretching it to the
	   form's height left a tall empty box beside a short list. */
	align-items: start;
	justify-content: start;
}

@media (max-width: 1100px) {
	.admin-panel .admin-grid-edit {
		grid-template-columns: minmax(0, 46rem);
	}
}

/* A product open takes the page.
 *
 * The list is for finding one and the search for naming one; once a product is
 * open, both have been used and the work is the product. The way back is the
 * breadcrumb, which is where a person looks for it. */
.admin-panel .admin-grid-edit.admin-edit-product-open {
	grid-template-columns: minmax(0, 1fr);
}

/* .admin-panel in the selector, because .admin-panel .admin-product-search
   sets display: grid further down the file and would win a tie. */
.admin-panel .admin-edit-product-open .admin-edited-box,
.admin-panel .admin-edit-product-open .admin-product-search {
	display: none;
}

.admin-edited-list {
	list-style: none;
	margin: 0.6rem 0 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	/* A ceiling, not a height: the box follows its content, but the endpoint
	   returns up to 200 rows and without this the panel would grow past the
	   screen. */
	max-height: 30rem;
	overflow-y: auto;
}

.admin-edited-empty {
	font-size: 0.82rem;
	color: rgba(244, 239, 230, 0.6);
}

/* A button filling its row, because the whole row is the target - a link-sized
   hit area inside a list is a miss waiting to happen. */
.admin-panel .admin-edited-item {
	/* The picture beside the facts: a product number is not a product, and the
	   list is read to check an edit. */
	display: flex;
	align-items: flex-start;
	gap: 0.75rem;
	width: 100%;
	margin: 0;
	padding: 0.5rem 0 0;
	background: none;
	border: 0;
	border-radius: 0;
	text-align: left;
	font: inherit;
	cursor: pointer;
}

/* The hover belongs to the whole row, from one line to the next: on the button
   alone it covered the product and stopped short of the story under it, so the
   shading ended in the middle of the thing being pointed at. */
.admin-panel .admin-edited-item:hover {
	background: none;
}

.admin-edited-row:hover {
	background: rgba(255, 255, 255, 0.06);
}


.admin-edited-code {
	display: block;
	font-weight: 700;
	color: var(--accent);
	font-size: 0.88rem;
}

.admin-edited-title {
	display: block;
	font-size: 0.82rem;
	line-height: 1.3;
}

.admin-grid-single .admin-box {
	padding: clamp(1.4rem, 2.2vw, 2rem);
}

.admin-grid-single .admin-field-name {
	font-size: 0.9rem;
}

.admin-grid-single .admin-file-text,
.admin-grid-single textarea.admin-file-text {
	font-size: 1rem;
	padding: 0.65rem 0.8rem;
}

/* --- Admin: tila, historia ja tuotteen muokkaus ------------------------- */

/* Admin menu. Outside the panel and right-aligned, the same place the shop's
   site-nav occupies - and the shop nav is hidden in admin mode, so the two
   never compete for the row.
   Buttons rather than links: these switch a section rather than navigate, and
   a link that does not change the address is a lie to a screen reader. */
.admin-nav {
	width: var(--content-width);
	margin: 1.2rem auto 0.9rem;
	display: flex;
	flex-wrap: wrap;
	justify-content: flex-end;
	/* Centre, not baseline: two of the items carry a badge and were therefore
	   taller than the rest, and on a baseline that difference showed as items
	   of different sizes sitting at different heights. */
	align-items: center;
	gap: 0.4rem 1.8rem;
}

.admin-nav-link {
	background: none;
	border: 0;
	border-radius: 0;
	padding: 0.2rem 0;
	margin: 0;
	/* One height for all of them, so a badge does not make its item bigger
	   than its neighbours. inline-flex keeps the badge on the text's own
	   middle inside that height. */
	display: inline-flex;
	align-items: center;
	/* 2.2rem is the badge (1.8) plus this button's own padding (2 × 0.2), so
	   an item with a badge and an item without come out exactly the same
	   height. At 2.1 they differed by a pixel, which is enough to see in a
	   row of seven. */
	min-height: 2.2rem;
	line-height: 1;
	/* White for the unselected items. At 0.62 they were legible only if you
	   looked for them, and a menu you have to hunt for is not a menu. */
	color: #fff;
	/* The shop's heading face, as everywhere a section is named: the back office
	   is part of the same shop, and a menu set in the body face read as a
	   different program's toolbar. Bebas has one weight, so the size and the
	   colour do the work that bold did. */
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.25rem;
	font-weight: 400;
	letter-spacing: 0.05em;
	cursor: pointer;
}

/* VERKKOKAUPPA set apart: the items before it switch a section within the
   admin view, this one leaves it. Same row, but the wider gap says they are
   not the same kind of thing. */
.admin-nav-exit {
	margin-left: 1.8rem;
}

.admin-nav-link:hover,
.admin-nav-link:focus-visible {
	color: var(--accent-soft);
	background: none;
}

/* Orange and underlined for the selected item, the same marking the shop uses.
   Note the trade-off, so nobody "fixes" it later: on a dark ground orange is
   darker than white, so the selected item is the lower-contrast one on the row
   (#f26a1b 4.8:1 against white 14.7:1). Both clear WCAG AA; the underline and
   the hue carry the selection rather than brightness.
   text-decoration rather than a border, so the line hugs the text instead of
   the button box. */
.admin-nav-active,
.admin-nav-active:hover {
	color: var(--accent);
	text-decoration: underline;
	text-decoration-thickness: 2px;
	text-underline-offset: 0.35em;
}

.admin-catalogue-buttons {
	display: flex;
	flex-wrap: wrap;
	/* Bottom edges, not centres: the link is much shorter than the button, and
	   centring left it floating in the middle of the button's height. */
	align-items: flex-end;
	gap: 1.4rem;
	margin-top: 1.1rem;
}

/* TARKISTA NYT sits under the dates it answers, at the right edge: the rows
   above are a label column and a value column, and a button in the flow would
   break that column in two. The padding is the row's own, not the button's -
   the button's size is written once in :root. */
.admin-catalogue-checkrow {
	display: flex;
	justify-content: flex-end;
	padding-top: 0.9rem;
}

/* .admin-panel button carries a margin-top for buttons that stand alone; here
   the row's padding does that work, and both would read as a gap twice over. */
.admin-panel .admin-catalogue-checkrow button {
	margin-top: 0;
}

/* HISTORIA keeps the right edge it had, now lining up under TARKISTA NYT.
   margin-left rather than justify-content: space-between, so a second control
   would land beside it instead of stretching the row apart. */
.admin-catalogue-buttons .admin-text-link {
	margin-left: auto;
}

/* .admin-panel button adds margin-top to every button; inside this row the gap
   handles the spacing, so the margin would push the two out of line. */
.admin-panel .admin-catalogue-buttons button {
	margin-top: 0;
}

/* A button that reads as a link. Used where the action reveals something on the
   page rather than submitting or navigating, so it should not compete with
   TARKISTA NYT for attention.
   Same marking as the menu: white while closed, orange and underlined once
   open. Smaller than body text, because it is a control beside content rather
   than a heading. */
.admin-text-link {
	background: none;
	border: 0;
	border-radius: 0;
	padding: 0 0 0.35rem;
	color: #fff;
	font: inherit;
	font-size: 0.76rem;
	font-weight: 700;
	letter-spacing: 0.06em;
	text-decoration: none;
	cursor: pointer;
}

.admin-panel .admin-text-link:hover,
.admin-panel .admin-text-link:focus-visible {
	background: none;
	color: var(--accent-soft);
}

/* Open state. Note the same trade-off as the admin menu: on this dark ground
   orange is lower contrast than white, so the underline and the hue carry the
   state rather than brightness. */
.admin-panel .admin-text-link-active,
.admin-panel .admin-text-link-active:hover {
	color: var(--accent);
	text-decoration: underline;
	text-decoration-thickness: 2px;
	text-underline-offset: 0.3em;
}

.admin-diff-more {
	margin: 0.7rem 0 0;
}

.admin-runs {
	margin-top: 1.2rem;
}

.admin-run-list,
.admin-diff-list {
	list-style: none;
	margin: 0.5rem 0 0;
	padding: 0;
}

/* Grid rather than flex: the detail panel needs to span the full width below
   both columns, and a flex row would push it into a third column. */
.admin-run-item {
	display: grid;
	grid-template-columns: 1fr auto;
	align-items: start;
	gap: 0.4rem 1rem;
	padding: 0.9rem 0;
	border-top: 1px solid rgba(255, 255, 255, 0.1);
	font-size: 0.9rem;
}

.admin-run-item > .admin-run-detail {
	grid-column: 1 / -1;
}

.admin-run-fields {
	opacity: 0.8;
}

/* New products highlighted: it is the run's only row that calls for a decision
   from the admin (image, title, visibility), and set in the same style among the
   field list it would be lost. */
.admin-run-added {
	color: var(--accent);
	font-weight: 600;
}

/* A visibility change: the same weight as new products, because both say what is
   in the shop right now - but not in the accent colour, so one run row is not two
   orange rows. */
.admin-run-visibility {
	font-weight: 600;
}

.admin-run-failed {
	color: #ff8a7a;
}

.admin-run-reverted {
	opacity: 0.6;
	text-decoration: line-through;
}

/* A hand-edited field and a skipped import change: the orange says this one does
   not follow the supplier. */
.admin-locked-note {
	color: var(--accent-mid);
	font-size: 0.82rem;
}

.admin-diff-list li {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.5rem;
	padding: 0.3rem 0;
	font-size: 0.85rem;
	border-top: 1px solid rgba(255, 255, 255, 0.07);
}

.admin-diff-code {
	font-variant-numeric: tabular-nums;
	opacity: 0.7;
	min-width: 5.5rem;
}

/* An article code opens the product for editing. A button rather than an <a>,
   because the admin sections are not routes but swapped in place (see
   showAdminView) - an <a> with no address would promise a link that is not
   there.

   The whole global button rule has to be undone: it gives a button the orange
   gradient, the rounded shape and padding, so without this every code would be a
   pill button at the start of the row. The selector starts from .admin-panel so
   it beats the panel's own button rules. */
.admin-panel .admin-diff-link {
	background: none;
	border: 0;
	border-radius: 0;
	padding: 0;
	margin: 0;
	width: auto;
	font: inherit;
	font-variant-numeric: tabular-nums;
	font-weight: 600;
	color: inherit;
	text-align: left;
	text-decoration: none;
	cursor: pointer;
}

/* Only the hover reveals that it is clickable: an underlined code on every row
   would make the list striped, and the rows come a hundred at a time. */
.admin-panel .admin-diff-link:hover,
.admin-panel .admin-diff-link:focus-visible {
	background: none;
	color: var(--accent);
	text-decoration: underline;
}

.admin-diff-old {
	opacity: 0.6;
	text-decoration: line-through;
}

/* The new value in the accent colour: it is the one thing on the row the admin
   is looking for - the old value is struck through beside it only for
   comparison. */
.admin-diff-new {
	color: var(--accent);
	font-weight: 700;
}

.admin-run-detail {
	margin-top: 0.9rem;
	padding: 1rem 1.1rem;
	border-radius: 10px;
	background: rgba(0, 0, 0, 0.35);
	max-height: 22rem;
	overflow-y: auto;
}

/* The detail opens under the button that opened it, so the gap above the
   buttons matters as much as the one below. */
/* MUUTOKSET to the bottom of its row, matching HISTORIA in the box above. The
   run item's grid is align-items: start, so without this the link sat level
   with the date at the top of a three-line row. */
.admin-run-item .admin-catalogue-actions {
	display: flex;
	gap: 0.6rem;
	align-self: end;
}

/* Sized to an article code, which is six characters. A field spanning the box
   promised something the input never needs. */
/* Width set by the placeholder it has to show, "Tuotenumero, esim. 749947",
   not by the six characters that get typed into it. A hint cut off mid-word is
   worse than no hint.
   The .admin-panel prefix is required, not decoration: .admin-form comes later
   in this file at the same specificity and sets grid-template-columns: 1fr, so
   without the prefix it would win and stack the field and the button.
   align-items: stretch so HAE takes the row height, which is the field's
   height - matching font metrics and paddings by hand drifts apart the moment
   either changes. */
/* The field takes the box, the way the orders page's search takes its own.
   It was cut to 13rem - 208 pixels for a product number in a box three times
   that wide - and read as a form that had not finished drawing. */
.admin-panel .admin-product-search {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	gap: 0.6rem;
	align-items: stretch;
}

/* Sized to the word and to the field. The base button's 1rem sides are meant
   for the shop's calls to action; on a three-letter label they were most of its
   width. Vertical padding is dropped because the grid row already gives the
   button the field's height, and a button centres its label. */
/* inline-flex centring rather than trusting a button's default text layout.
   The button is stretched to the row height, and at that point where the label
   sits is up to the browser's own button metrics - which is what left it
   sitting off centre against the field. */
.admin-panel .admin-product-search button {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	padding: 0 0.85rem;
	font-size: 0.9rem;
	line-height: 1;
}

/* An empty status line still takes a line box and its own margin, so it left
   about 1.9rem of dead space under the search row - which is why the row read
   as sitting above centre in the box rather than in it. Gone when empty, back
   the moment it has something to say. */
.admin-status-line:empty {
	display: none;
}

/* Symmetric room around the search row, replacing .admin-form's margin-top,
   which put the space on one side only. */
.admin-panel .admin-product-search {
	margin: 0.7rem 0;
}

/* One explicit height for the row, given to the field. The button is stretched
   to match it, so the two are the same height by construction rather than by
   two paddings happening to agree.
   Padding is horizontal only: a single-line text input centres its own text in
   whatever height it is given, and vertical padding would fight that. */
.admin-panel .admin-product-search .admin-file-text {
	height: 2.4rem;
	box-sizing: border-box;
	padding: 0 0.6rem;
	font-size: 0.9rem;
	line-height: 1.2;
}

/* HAE keeps the standard button size. Only the form's own buttons are scaled
   down - they sit among labelled fields rather than starting an action. */
.admin-panel .admin-product-fields .admin-catalogue-actions button {
	font-size: 0.82rem;
	padding: 0.45rem 0.9rem;
}

.admin-field {
	display: block;
	margin-bottom: 0;
}

/* Two columns for the field form. Twenty-two fields in one stack is a long
   scroll, and most of the values are short.
   Scoped to .admin-product-fields, not .admin-product-box .admin-form: the
   search row carries .admin-form too, so the descendant selector outranked
   .admin-product-search and stretched the field and HAE to half the box each.
   That is why they stayed full width no matter what widths were set. */
.admin-panel .admin-product-fields {
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 0.8rem 1.2rem;
	align-items: start;
}

/* One column when there is no room for two. */
@media (max-width: 720px) {
	.admin-panel .admin-product-fields {
		grid-template-columns: minmax(0, 1fr);
	}
}

/* Kuvaus and the button row take the full width. */
.admin-field-wide {
	grid-column: 1 / -1;
}

/* A read-only value, rendered as text rather than a disabled input. A greyed
   out field invites a click and then does nothing; this reads as a value. */
.admin-field-static {
	margin: 0;
	padding: 0.55rem 0;
	color: rgba(244, 239, 230, 0.9);
	word-break: break-word;
}

.admin-field-readonly .admin-field-name {
	opacity: 0.5;
}

/* Markup and the raw source row, shown as source. Scrolls in its own box so a
   long line cannot widen the panel. */
.admin-field-code {
	font-family: ui-monospace, "SF Mono", Menlo, monospace;
	font-size: 0.75rem;
	line-height: 1.45;
	white-space: pre-wrap;
	word-break: break-word;
	max-height: 12rem;
	overflow-y: auto;
	background: rgba(0, 0, 0, 0.3);
	border-radius: 8px;
	padding: 0.6rem 0.7rem;
}

/* Everything stored about the product that is not a products column. */
.admin-extras {
	margin-top: 1.4rem;
	padding-top: 1.4rem;
	border-top: 1px solid rgba(255, 255, 255, 0.14);
}

.admin-image-row {
	display: block;
	word-break: break-all;
}

.admin-raw {
	margin-top: 1rem;
}

.admin-raw summary {
	cursor: pointer;
	color: #fff;
	font-size: 0.76rem;
	font-weight: 700;
	letter-spacing: 0.06em;
}

.admin-raw summary:hover {
	color: var(--accent-soft);
}

.admin-raw[open] summary {
	color: var(--accent);
	margin-bottom: 0.6rem;
}

/* Product image beside its identifying facts. */
.admin-product-head {
	display: flex;
	flex-wrap: wrap;
	gap: 1.2rem;
	align-items: flex-start;
	/* A rule and real space after the search row. Searching and reading the
	   result are two separate acts, and without the break the product looked
	   like part of the form that found it. */
	margin-top: 2rem;
	padding-top: 2rem;
	border-top: 1px solid rgba(255, 255, 255, 0.14);
}

.admin-product-image {
	width: 10rem;
	height: 10rem;
	object-fit: contain;
	border-radius: 10px;
	background: rgba(255, 255, 255, 0.9);
	flex: none;
}

/* A URL that is in the catalogue but gone from the CDN. Without this the box
   showed the browser's broken-image glyph, which reads as our bug. */
.admin-product-image-broken {
	background: rgba(255, 255, 255, 0.06);
	position: relative;
}

.admin-product-image-broken::after {
	content: "kuva puuttuu";
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	color: rgba(244, 239, 230, 0.5);
	font-size: 0.82rem;
}

.admin-product-image-empty {
	display: flex;
	align-items: center;
	justify-content: center;
	background: rgba(255, 255, 255, 0.06);
	color: rgba(244, 239, 230, 0.5);
	font-size: 0.82rem;
}

.admin-product-facts {
	flex: 1 1 14rem;
	min-width: 0;
}

.admin-field-name {
	display: block;
	font-size: 0.82rem;
	opacity: 0.75;
	margin-bottom: 0.2rem;
}

/* The edit note and VAPAUTA on the same row: the text says the field has been
   detached from the feed, and the button is what puts it back - they belong
   together. The button at the right edge, so it is in the same place on every
   field regardless of the text's length. */
.admin-field-lock {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 0.8rem;
	margin-top: 0.35rem;
	color: var(--accent-mid);
	font-size: 0.8rem;
}

.admin-field-lock-text {
	min-width: 0;
}

/* Larger than before (0.7rem): it is the row's only action, and too small a
   button read as part of the explanation rather than as a control. */
.admin-panel .admin-field-lock .admin-lock-release {
	margin: 0;
	padding: 0.4rem 0.9rem;
	font-size: 0.82rem;
	letter-spacing: 0.05em;
	white-space: nowrap;
	flex: none;
}

/* Warning shown while a field is edited but not yet saved. Same colour as the
   lock badge it is about to become, so the two read as one story. */
.admin-field-pending {
	display: block;
	margin-top: 0.25rem;
	color: var(--accent-mid);
	font-size: 0.8rem;
	font-weight: 700;
}

/* The standing explanation above the fields. A left rule rather than a box,
   so it reads as a note about the form rather than a fourth control in it. */
.admin-lock-info {
	margin: 0 0 0.4rem;
	padding: 0 0 0 0.9rem;
	border-left: 2px solid var(--accent);
	font-size: 0.82rem;
	color: rgba(244, 239, 230, 0.78);
	display: flex;
	flex-direction: column;
	gap: 0.35rem;
}

.admin-lock-info p {
	margin: 0;
	max-width: 46rem;
}

.admin-lock-info-title {
	color: #fff;
	font-weight: 700;
	letter-spacing: 0.02em;
}

/* Pienet napit tekstin seassa: vapauta-lukitus ja koodiehdotus. */
.admin-field-lock .ghost,
.admin-suggest {
	padding: 0.1rem 0.5rem;
	font-size: 0.78rem;
}

.admin-product-history {
	margin-top: 1rem;
	max-height: 18rem;
	overflow-y: auto;
}

.admin-box {
	padding: clamp(1.2rem, 1.8vw, 1.6rem);
}

/* Vali tekstin ja napin valissa. Napit olivat kiinni ylla olevassa
   tekstissa, mika teki lohkoista yhden massan. */
.admin-panel button {
	margin-top: 1rem;
}

.admin-panel .admin-catalogue-actions button,
.admin-panel .admin-field-lock button,
.admin-panel .admin-product-search button,
.admin-panel .admin-suggest {
	margin-top: 0;
}

.admin-panel .admin-catalogue-actions {
	display: flex;
	gap: 0.6rem;
	margin-top: 1rem;
}

.admin-panel .eyebrow {
	margin-top: var(--admin-eyebrow-gap);
}

.admin-panel .admin-box > .eyebrow:first-child {
	margin-top: 0;
}

.admin-panel .admin-catalogue-list,
.admin-panel .admin-run-list {
	margin-top: 0.8rem;
}

.admin-main-box h2 {
	margin-top: 0.2rem;
}

.admin-session-row {
	margin-top: 0.4rem;
	display: grid;
	gap: 0.5rem;
}

.admin-session-row .description {
	margin: 0;
}

.admin-form {
	margin-top: 1rem;
	display: grid;
	grid-template-columns: 1fr;
	gap: 0.7rem;
	align-items: center;
}

.admin-file-text {
	background: rgba(255, 255, 255, 0.95);
	color: var(--ink);
	font-weight: 500;
}

.admin-select {
	width: 100%;
	border: 1px solid rgba(24, 21, 17, 0.18);
	background: rgba(255, 255, 255, 0.92);
	border-radius: 12px;
	padding: 0.7rem 0.8rem;
	font: inherit;
	color: var(--ink);
	font-weight: 500;
}

.admin-poll-row {
	display: grid;
	grid-template-columns: 1fr;
	align-items: center;
	gap: 0.65rem;
	padding: 1rem 1.05rem;
	border: 1px solid rgba(24, 21, 17, 0.18);
	background: #fff;
	border-radius: 12px;
}

.admin-checkbox-row {
	display: flex;
	align-items: center;
	gap: 0.65rem;
	font-size: 0.95rem;
	font-weight: 500;
	color: var(--ink);
	padding: 0;
	border: 0;
	background: transparent;
	border-radius: 0;
}

.admin-checkbox-row input,
.admin-promo-option input[type="checkbox"] {
	appearance: auto;
	-webkit-appearance: checkbox;
	width: 1rem;
	height: 1rem;
	margin: 0;
	padding: 0;
	background: #fff;
	border: 1px solid rgba(24, 21, 17, 0.35);
	border-radius: 3px;
	accent-color: var(--accent);
	cursor: pointer;
}

.admin-poll-select {
	width: 100%;
	min-width: 0;
	background: #fff;
	padding: 0.72rem 0.8rem;
	color: var(--muted);
}

.admin-poll-select:disabled {
	color: rgba(92, 81, 71, 0.58);
}

.admin-file-text::placeholder {
	color: var(--muted);
	opacity: 1;
}

.admin-auth-section {
	margin: 0.1rem 0;
	display: grid;
	gap: 0.55rem;
}

.admin-auth-fields {
	display: grid;
	grid-template-columns: 1fr;
	gap: 0.55rem;
}


#adminUploadBtn {
	margin-top: 0.9rem;
}

.admin-loaded {
	position: sticky;
	top: 0;
}

.admin-catalogue-list {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: 0.75rem;
}

/* No card of its own: the surrounding .admin-main-box is the catalogue's box,
   and the item used to carry its own background and border, which read as a
   box inside a box once the wrapper heading was removed. The frame comes back
   only when there is more than one catalogue to separate. */
.admin-catalogue-item {
	display: flex;
	/* Column, not row: the meta block is a stack of rows now, so a horizontal
	   layout would squeeze it into a narrow column beside the name. */
	flex-direction: column;
	align-items: stretch;
	gap: 0.45rem;
}

.admin-catalogue-list:has(.admin-catalogue-item + .admin-catalogue-item) .admin-catalogue-item {
	padding: 0.75rem;
	border-radius: 10px;
	background: rgba(4, 10, 24, 0.55);
	border: 1px solid rgba(173, 193, 255, 0.24);
}

/* The supplier's name is the box title now that the KATALOGI eyebrow is gone. */
.admin-catalogue-name {
	margin: 0;
	font-size: 1.15rem;
	font-weight: 800;
	letter-spacing: 0.02em;
	text-transform: uppercase;
	color: #f8fbff;
}

/* One fact per row, label and value in two aligned columns.
   This was a plain block of inline spans, so all six facts ran together as one
   sentence. A <dl> is the right element - each row is a term and its value -
   and the grid keeps the values on a shared left edge so the column can be
   scanned instead of read. */
.admin-catalogue-meta {
	margin: 0.35rem 0 0;
	font-size: 0.82rem;
	color: #ffffff;
	display: flex;
	flex-direction: column;
	gap: 0.3rem;
}

.admin-catalogue-row {
	display: grid;
	/* The widest label is "Viimeisin muutos"; a fixed column keeps the values
	   aligned across rows even when a label is short. */
	grid-template-columns: 10.5rem minmax(0, 1fr);
	gap: 0.5rem;
	align-items: baseline;
}

.admin-catalogue-row dt {
	margin: 0;
	color: rgba(255, 255, 255, 0.62);
	letter-spacing: 0.02em;
}

.admin-catalogue-row dd {
	margin: 0;
	font-weight: 600;
}

.admin-catalogue-delete {
	white-space: nowrap;
}

.admin-catalogue-actions {
	display: flex;
	gap: 0.5rem;
	align-items: center;
}

body.admin-mode .results-card {
	display: none !important;
}

body.admin-mode .profile-card {
	position: static;
	width: 0;
	height: 0;
	padding: 0;
	border: 0;
	background: transparent;
	box-shadow: none;
	overflow: visible;
}

body.admin-mode .app-shell {
	width: var(--content-width);
	margin-top: 0;
}

/* The admin box sits in the same place and at the same size as the shop's main
   box: it is inside .app-shell as a sibling of .glass-box, so in normal flow it
   lands exactly where that one was (in admin mode glass-box is display: none). The
   same width also keeps the profile box aligned - the earlier fixed 1320px was
   wider than the content width, so the profile looked indented against it. */
body.admin-mode .admin-panel {
	position: static;
	width: 100%;
	max-width: none;
	margin: clamp(1.5rem, 5vh, 3.5rem) auto 0;
	/* The same minimum height as .glass-box, so the box is the same size rather than
	   merely the same width. The content may grow it. */
	min-height: clamp(240px, 45vh, 520px);
	max-height: none;
	overflow: visible;
}

.add-garage-btn:disabled {
	opacity: 0.5;
	cursor: not-allowed;
}

.admin-file-row {
	display: flex;
	align-items: center;
	gap: 0.6rem;
	flex-wrap: wrap;
	font-size: 0.9rem;
	color: var(--muted);
}

.admin-file-label {
	font-weight: 700;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	font-size: 0.78rem;
}

.admin-summary {
	margin: 0.6rem 0 0;
	display: grid;
	gap: 0.28rem;
}

.admin-summary-row {
	display: grid;
	grid-template-columns: minmax(9rem, 12rem) 1fr;
	gap: 0.6rem;
	align-items: baseline;
}

.admin-summary-row dt {
	margin: 0;
	font-size: 0.76rem;
	font-weight: 700;
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: var(--muted);
}

.admin-summary-row dd {
	margin: 0;
	font-size: 0.94rem;
	color: var(--ink);
	/* Raw feed values include long HTML blobs and CDN URLs with no spaces to break on. */
	overflow-wrap: anywhere;
}


/* --- Front page work in progress: nothing is fetched from the database, so the
   sections that depend on the catalogue are hidden. Remove this block when the
   shop is switched back on. --- */
.results-card,
.profile-card,
.shop-empty-box {
	display: none !important;
}

.topbar {
	flex-direction: column;
	align-items: flex-start;
}


.search-icon {
	display: block;
	width: 1.15rem;
	height: 1.15rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 2;
	stroke-linecap: round;
}


/* The same glassy look as the search and login boxes: a translucent white film
   against black, a thin border and the same 18px rounding. */
/* --- Site level menu ------------------------------------------------------------- */
/*
   To the right edge above the box. The cart sits above this with a negative
   margin beside the search row, so the two do not collide.
   */
/* The button and the cart on the same row: that way they sit at the same height
   by structure rather than by guessed margins. The row carries the margin that
   used to push the cart to the box's edge. */
.finder-cart-row {
	align-self: stretch;
	width: 100%;
	display: flex;
	align-items: center;
	justify-content: space-between;
	/* The row wraps, and that is not only a narrow-screen concern.
	
	   With a bike chosen the row carries four things - the bike, UNIVERSAALIT
	   MUKANA, LISÄÄ ILMAN VUOSIMERKINTÄÄ and the cart - and the chips do not
	   shrink: their text is the whole point of them. Without wrapping the row was
	   simply forced wider than the page, which pushed the cart off the right edge
	   and gave the whole page a sideways scrollbar. Measured at 1000 px: the row
	   ran to 1150.
	
	   Wrapped, the cart drops to a line of its own instead, and margin-left: auto
	   below pins it to the same right edge the box under it has. */
	flex-wrap: wrap;
	/* Tighter than it was (1.5rem): the row has four things on it now, and every
	   millimetre here is one the chips do not have. */
	gap: 0.6rem 0.9rem;
	/* The top margin is at the same time the gap to the search field. Because the
	   button, the cart and the box follow one another in the flow, raising this moves
	   all three down by the same amount - which is why the spacing keeps its
	   proportions. */
	margin: 2rem 0 1.5rem;
	/* .topbar has z-index: 3 and reaches over the rows above, so a stacking context
	   of our own keeps it from swallowing clicks. */
	position: relative;
	z-index: 5;
}

.bike-finder-wrap {
	position: relative;
	flex: 0 0 auto;
	margin-left: var(--brand-group-shift);
	/* The garage chip takes a line of its own below the bike (see .garage-chip),
	   so the chips have to be allowed to wrap among themselves. */
	flex-wrap: wrap;
	row-gap: 0.5rem;
	/* And the box may not grow past the row it sits in. The line break inside it
	   has a flex basis of 100%, which counts towards this box's own widest
	   content - without a cap that pushed the page wider than the window.
	
	   The cap subtracts the group's own indent: the box starts that far in, so
	   100% of the row would end exactly that far past its right edge. */
	max-width: calc(100% - var(--brand-group-shift));
	/* The bike chip and the universal button side by side. The dropdown is absolute,
	   so it is not in this flow. */
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
}

.site-nav {
	/* Centred against the search field's height. It used to be flex-end, which
	   overrode the search row's align-items: center and left the menu at the bottom of
	   the row. */
	align-self: center;
	display: flex;
	flex-wrap: wrap;
	justify-content: flex-end;
	align-items: baseline;
	gap: 0.35rem 0.8rem;
	/* On the same row as the search field, at the right edge. Eight links take about
	   760 px and the search 320 px, so they fit inside the 1 120 px content area. If
	   the menu grows it wraps into its own space with flex-wrap rather than running
	   over the search field - which is what it used to do. */
	flex: 0 1 auto;
	margin: 0;
	position: relative;
	z-index: 5;
}

/* The Admin item in the shop menu is a button, not a link - it switches surface
   rather than navigating. These resets undo the global button styling so it
   matches its <a> siblings exactly. */
.site-nav-admin {
	background: none;
	border: 0;
	border-radius: 0;
	padding: 0;
	margin: 0;
	font: inherit;
	cursor: pointer;
}

.site-nav-admin:hover {
	background: none;
}

.site-nav-link {
	font-family: "Bebas Neue", sans-serif;
	/* The other sections smaller: the shop is where people land. */
	font-size: 0.98rem;
	letter-spacing: 0.05em;
	/* White rather than dimmed. 0.6 made the unselected items hard to pick out,
	   and the same complaint applies here as in the admin menu. */
	color: #fff;
	opacity: 1;
	text-decoration: none;
	white-space: nowrap;
	transition: color 0.12s ease, transform 0.14s ease;
}

/* Hover in the main menu: the same gesture as in the product menu
   (.front-menu). The item under the cursor grows and turns orange, the others
   shrink and dim - the one being opened stands out from the whole row rather than
   only from itself.

   This used to hold ".site-nav-link:hover { opacity: 1 }", which did nothing: in
   the base state opacity is already 1. So the hover existed in name but not in
   effect.

   The growth is by transform rather than font-size: the menu shares a row with the
   search field and wraps when it has to, so changing the font size would re-lay the
   row and the neighbours would jump. The links are flex children, so they are
   block-like and transform bites - on an inline element it would not. */
.site-nav:hover .site-nav-link {
	color: rgba(255, 255, 255, 0.45);
	transform: scale(0.9);
}

/* The same specificity (0,3,0) as the rule above, so it wins by order. */
.site-nav .site-nav-link:hover,
.site-nav .site-nav-link:focus-visible {
	color: var(--accent);
	transform: scale(1.16);
}

/* Verkkokauppa is larger and at full brightness even when unselected, because it
   is the shop's own section - the selection is marked by the accent colour and an
   underline. */
/* The shop is not one section among the others: it is where the visitor
   already is, and the rest are the dealer's other business areas. Set larger
   for that reason, and given a gap of its own so the size does not read as
   "Verkkokauppa Huolto" run together - the row's own 0.8rem is the spacing
   between siblings, and this link is not one. */
.site-nav-shop {
	font-size: 1.4rem;
	opacity: 1;
	margin-right: 1.4rem;
}

/* The selected page stays orange and underlined regardless of what is hovered.
   Three selectors, because ".site-nav-active" on its own (0,1,0) lost to the
   dimming (0,3,0) - the selection would have faded the moment the cursor entered
   the menu. All of them AFTER the hover rules, so the order keeps them in
   force. */
.site-nav-active,
.site-nav:hover .site-nav-active,
.site-nav .site-nav-active:hover {
	color: var(--accent);
	opacity: 1;
	text-decoration: underline;
	text-underline-offset: 0.25em;
}

/* The selected one does not shrink with the rest: it is the row's anchor. Hovered
   itself it grows like the others (the rule above comes before this one). */
.site-nav:hover .site-nav-active {
	transform: none;
}

.site-nav .site-nav-active:hover {
	transform: scale(1.16);
}

@media (prefers-reduced-motion: reduce) {
	.site-nav-link {
		transition: color 0.12s ease;
	}
}

.glass-box {
	/* The chat bubble is positioned against this (see .chat-widget). The box had no
	   position value, and nothing inside it is absolutely positioned against a more
	   distant ancestor - the submenu's popup aligns to .front-category-item. */
	position: relative;
	width: 100%;
	/* The subtraction matches the cart's top margin: the cart moves closer to the box
	   without the box itself moving. */
	margin: calc(clamp(1.5rem, 5vh, 3.5rem) - 1.95rem) auto 0;
	min-height: clamp(240px, 45vh, 520px);
	padding: clamp(0.85rem, 1.6vw, 1.2rem);
	border-radius: 18px;
	border: 1px solid rgba(255, 255, 255, 0.18);
	/* More opaque than the glass look was at first, so the background image does not
	   take attention from the content. Darker rather than lighter: the box's text is
	   light, so adding white would have raised the opacity and taken the contrast away
	   at the same time. Longhand, because the shorthand would reset
	   background-image. */
	background-color: rgba(10, 10, 10, 0.55);
	box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
	/* The content sits against a black ground, so the default colour is light rather
	   than the body's dark. */
	color: #f4efe6;
}

/* --- Adminin saapuneet kyselyt --------------------------------------------- */
/*
   The same speech bubble as on the customer side (.chat-bubble), because this is
   the other end of the same conversation. What differs is the position and the
   count.

   position: fixed to the view's bottom right: the bubble is OUTSIDE the box rather
   than on its edge as on the customer side. Fixed also because the admin panel
   scrolls - a bubble inside the box would disappear from view once a long list is
   scrolled down.

   The element lives inside #adminPanel, so it appears in all four sections and
   disappears when the admin view is left. No visibility logic of its own is
   needed.
   */
/* Visibility comes from the role: the markup starts with the .hidden class and
   applyProfileMode removes it for the admin. That lives there rather than here,
   because .hidden is already the whole application's way of hiding things - two
   mechanisms for the same thing would drift apart.

   Without the initial class the bubble would flash for every user while the page
   loads, before the profile has been read. */
.admin-inbox {
	position: fixed;
	right: clamp(1rem, 3vw, 2.25rem);
	bottom: clamp(1rem, 3vh, 2rem);
	/* The profile box is 20, so the bubble stays below it. The rest of the admin
	   content is below 10. */
	/* Above the login button and the profile card, both of which are 20. The
	   bubble floats and is always in the same corner, so it inevitably ends up
	   over them - and while it is open, it is the thing being used. */
	z-index: 25;
}

/* The bubble itself: the base shape comes from .chat-bubble, this only places it
   and draws attention. */
.admin-inbox-bubble {
	position: relative;
}

/* The attention signal: a pulsing glow around the bubble rather than a blink of
   opacity. A blinking element is hard to look at and disappears entirely at times,
   which looks like a fault; the glow stays readable throughout while still drawing
   the eye. */
@keyframes admin-inbox-syke {
	0%   { box-shadow: 0 10px 24px rgba(0, 0, 0, 0.4), 0 0 0 0 rgba(242, 106, 27, 0.55); }
	70%  { box-shadow: 0 10px 24px rgba(0, 0, 0, 0.4), 0 0 0 1.1rem rgba(242, 106, 27, 0); }
	100% { box-shadow: 0 10px 24px rgba(0, 0, 0, 0.4), 0 0 0 0 rgba(242, 106, 27, 0); }
}

/* The class comes from JS rather than sitting on the bubble: the pulse stops once
   the inbox has been opened, and after that the bubble is an ordinary button. */
.admin-inbox-bubble.admin-inbox-uusia {
	animation: admin-inbox-syke 2s ease-out infinite;
}

/* The count in the bubble's top right corner. A white ring, so the number stands
   out from the orange bubble even when the glow is at its brightest. */
.admin-inbox-count {
	position: absolute;
	top: -0.5rem;
	right: -0.5rem;
	min-width: 2rem;
	height: 2rem;
	padding: 0 0.4rem;
	display: grid;
	place-items: center;
	border-radius: 999px;
	border: 2px solid #14110f;
	background: #fff;
	color: var(--accent-deep);
	font-family: "Space Grotesk", sans-serif;
	font-size: 1.05rem;
	font-weight: 700;
	line-height: 1;
	/* Tasavalinen numero: kaksinumeroinen luku ei siirra merkkia sivusuunnassa
	   kun maara muuttuu. */
	font-variant-numeric: tabular-nums;
}

/* Zero is shown, but quietly: a white badge with an orange number reads as
   urgent, and zero is not urgent. A grey ground and a dimmer number carry the same
   information without the call for attention. */
.admin-inbox-count[data-tyhja="1"] {
	background: rgba(255, 255, 255, 0.22);
	border-color: rgba(255, 255, 255, 0.3);
	color: #f4efe6;
	opacity: 0.75;
}

/* The panel opens above the bubble, aligned to the right edge. The base look
   comes from .chat-panel; what is overridden here is the customer side's
   placement, which is computed against its own bubble. */
.admin-inbox-panel {
	right: 0;
	bottom: calc(100% + var(--bubble-gap));
	max-height: calc(100vh - 10rem);
}

.admin-inbox-list {
	margin: 0.9rem 0 0;
	padding: 0;
	list-style: none;
	display: grid;
	gap: 0.7rem;
	/* The log flexes, the title and the demo note do not - the same split as in the
	   customer's chat. */
	flex: 0 1 auto;
	min-height: 0;
	overflow-y: auto;
}

.admin-inbox-list li {
	padding: 0.65rem 0.75rem;
	border-radius: 12px;
	border: 1px solid rgba(255, 255, 255, 0.14);
	background: rgba(255, 255, 255, 0.05);
}

.admin-inbox-question {
	margin: 0;
	font-size: 0.92rem;
	line-height: 1.4;
}

.admin-inbox-meta {
	margin: 0.3rem 0 0;
	font-size: 0.76rem;
	opacity: 0.55;
}

/* The customer's own words, kept as they were typed: the line breaks are part
   of what they wrote, and a message about two parts loses which line goes with
   which without them. */
.admin-inbox-question {
	white-space: pre-wrap;
	overflow-wrap: anywhere;
}

/* The attached products as their article codes. A code is something to search
   the catalogue with, which a line of prose is not - so they are repeated here
   even though they are already in the message text. */
.admin-inbox-skus {
	margin: 0.35rem 0 0;
	display: flex;
	flex-wrap: wrap;
	gap: 0.3rem;
}

/* A chip that is also a link to the product's own page. It keeps the chip's
   shape rather than looking like a line of link text: it is one of several in a
   row, and underlined blue words in a row read as a sentence somebody broke. */
.admin-inbox-sku {
	display: inline-block;
	padding: 0.1rem 0.4rem;
	border-radius: 999px;
	border: 1px solid rgba(255, 255, 255, 0.18);
	color: inherit;
	font-size: 0.72rem;
	font-variant-numeric: tabular-nums;
	text-decoration: none;
}

.admin-inbox-sku:hover,
.admin-inbox-sku:focus-visible {
	border-color: var(--accent);
	color: var(--accent);
}

/* The bike and the topic the customer had chosen. Dimmer than the message: it
   is context for the answer rather than something they said. */
.admin-inbox-context {
	margin: 0.3rem 0 0;
	font-size: 0.74rem;
	opacity: 0.5;
}

/* The address is the answer: a mailto opens the reply in the dealer's own mail
   client, with the address and a subject already in place. */
.admin-inbox-mailto {
	color: var(--accent);
	text-decoration: none;
}

.admin-inbox-mailto:hover,
.admin-inbox-mailto:focus-visible {
	text-decoration: underline;
}

/* The notification to the dealer did not leave. Worth saying here, because this
   view is then the only place the message exists for them. */
.admin-inbox-warn {
	color: var(--accent-soft);
	opacity: 1;
}

/* The cross that takes a row off the list. In the row's own corner, like the
   panel's close button in the panel's - the same gesture in the same place
   means the same thing, and the row has to make room for it (padding-right on
   the li below). */
.admin-inbox-dismiss {
	float: right;
	margin: -0.2rem -0.3rem 0 0.4rem;
	padding: 0 0.3rem;
	border: 0;
	border-radius: 6px;
	background: none;
	color: inherit;
	font-size: 1.1rem;
	line-height: 1.2;
	opacity: 0.45;
	cursor: pointer;
}

.admin-inbox-dismiss:hover,
.admin-inbox-dismiss:focus-visible {
	background: none;
	opacity: 1;
	color: var(--accent);
}

/* The tool button on a catalogue's name: two sliders, the shape everything
   uses for settings. On the name's row and pushed to its end, so the names stay
   a column the eye can run down. */
.admin-catalogue-name {
	display: flex;
	align-items: center;
	gap: 0.5rem;
}

.admin-catalogue-tool {
	margin-left: auto;
	padding: 0.2rem;
	border: 0;
	border-radius: 8px;
	background: none;
	color: inherit;
	opacity: 0.5;
	cursor: pointer;
	line-height: 0;
}

.admin-catalogue-tool svg {
	width: 1.05rem;
	height: 1.05rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.8;
	stroke-linecap: round;
}

.admin-catalogue-tool:hover,
.admin-catalogue-tool:focus-visible,
.admin-catalogue-tool-on {
	background: none;
	opacity: 1;
	color: var(--accent);
}

/* The connection, once opened. Set apart from the numbers above it by a line
   rather than a box: it belongs to the same catalogue, it is just a different
   question about it. */
.admin-catalogue-connection {
	margin-top: 0.6rem;
	padding-top: 0.6rem;
	border-top: 1px solid rgba(255, 255, 255, 0.14);
}

/* Money that went back, in the month's row. Marked rather than folded into
   the sum: a month with heavy sales and heavy returns is not a good month, and
   one number cannot say both. */
.admin-month-returned {
	color: var(--accent-soft);
	font-size: 0.8rem;
}

/* The part of an order line that came back. In the accent rather than dimmed:
   on a list of lines it is the one thing that changed after the order was
   placed. */
.admin-order-returned {
	color: var(--accent-soft);
}

/* The wholesaler on an order line. Dimmer than the rest of the line: it is
   where the dealer orders the part from, not part of what the customer
   bought. */
/* The wholesaler is read, not glanced past.
 *
 * It used to sit at 0.6 opacity among the dots, which made the one field that
 * decides where the part is ordered from the faintest thing on the line. The
 * dealer looks this up on every order. */
.admin-order-supplier {
	font-weight: 700;
}

/* --- Tulostus: osoitelappu ja kuitti ------------------------------------- */

/* The document is shown on screen first and printed second.
 *
 * A browser cannot print without its own dialog, so the paper comes up here
 * the moment the button is pressed - drawn as it will print - and the dialog
 * only appears if TULOSTA is pressed. What the dealer wanted to see is then
 * immediate, and the step that asks for confirmation is the one that uses
 * paper.
 */
#printArea {
	position: fixed;
	inset: 0;
	z-index: 120;
	display: grid;
	place-items: center;
	align-content: center;
	gap: 0.8rem;
	padding: 1.5rem;
	overflow-y: auto;
	background: rgba(10, 8, 6, 0.75);
	backdrop-filter: blur(3px);
}

/* The paper, drawn as paper: white ground and black ink, because that is what
   comes out of the printer - a preview in the shop's own dark colours would
   show something the printer will not produce. */
.tuloste-paperi {
	width: min(20cm, 100%);
	max-height: 78vh;
	overflow-y: auto;
	background: #fff;
	color: #111;
	border-radius: 4px;
	box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.5);
}

/* Postin osoitekortti. Sama paikka ja sama koko kuin kaupan omalla paperilla,
   mutta sisältö on PDF eikä HTML - selaimen oma katselin piirtää sen kehyksen
   sisälle. Valkoinen tausta siltä varalta että PDF latautuu hitaasti: harmaa
   laatikko tummalla pohjalla näyttää rikkinäiseltä. */
.tuloste-pdf {
	width: min(20cm, 100%);
	height: 78vh;
	border: 0;
	border-radius: 4px;
	background: #fff;
	box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.5);
}

/* Varateksti sille selaimelle joka ei näytä PDF:ää upotettuna: se piirtyy
   objektin sisälle vain silloin kun upotus epäonnistuu. */
.tuloste-pdf-apu {
	margin: 0;
	padding: 2rem;
	color: #111;
	text-align: center;
}

.tuloste-napit {
	display: flex;
	align-items: center;
	gap: 1rem;
}

/* Sama mitta kuin TULOSTA-napilla, koska ne ovat vierekkäin ja tekevät saman
   työn eri tavalla. Linkki eikä nappi, koska se lataa tiedoston. */
.tuloste-lataa {
	padding: var(--admin-button-padding);
	border: 1px solid currentColor;
	border-radius: 8px;
	font-size: var(--admin-button-size);
	line-height: var(--admin-button-line);
	letter-spacing: 0.04em;
	text-decoration: none;
	color: inherit;
}

.tuloste-lataa:hover,
.tuloste-lataa:focus-visible {
	color: var(--accent);
}

.tuloste-sulje {
	color: #f4efe6;
}

/* The shop's name on both documents.
 *
 * The same face as the shop's own heading (Bebas Neue), because a receipt and
 * a parcel are the two pieces of the shop a customer holds in their hand, and
 * they should look like it came from the same place as the website.
 *
 * A wordmark rather than a picture: there is no logo file yet, and set in this
 * face the name carries perfectly well. When a real mark arrives it replaces
 * this one line.
 */
.print-logo {
	margin: 0 0 0.2cm;
	/* The shop's own face, the one the site's heading uses.
	   Bebas Neue has a single weight, so it is set at 400 and given size
	   instead: asking a browser for a bold it does not have makes it smear the
	   letters itself, and on paper that reads as a blur rather than as
	   weight. */
	font-family: "Bebas Neue", sans-serif;
	font-weight: 400;
	letter-spacing: 0.05em;
	line-height: 1;
	color: #111;
}

/* The shop on the left, the document on the right, sharing a baseline. */
.print-otsikko {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 1cm;
	margin-bottom: 0.2cm;
}

.print-otsikko .print-logo {
	margin: 0;
}

.print-otsikko h1 {
	margin: 0;
	font-family: "Bebas Neue", sans-serif;
	font-weight: 400;
	letter-spacing: 0.12em;
	color: #444;
	/* The heading takes the right column and ends where the column ends -
	   over the figures, which are the other thing on this paper that lines up
	   on the right. */
	flex: 0 0 var(--print-oikea, 6.5cm);
	text-align: right;
}

.print-label .print-logo {
	font-size: 38pt;
	/* Keskelle lapun yläreunaan.
	 *
	 * Osoitelappu luetaan käsivarren päästä pöydällä muiden pakettien seassa,
	 * ja nimi kertoo kenen paketti se on ennen kuin mikään muu siinä merkitsee.
	 * Keskitettynä se on otsikko eikä ensimmäinen rivi osoitetta: vastaanottajan
	 * lohko alkaa vasemmasta reunasta sen alta, jolloin kaksi asiaa erottuvat
	 * toisistaan ilman viivaa väliin. */
	text-align: center;
}

.print-receipt .print-logo {
	font-size: 30pt;
}

/* The documents themselves. The same rules on screen and on paper, so the
   preview is not a second design that can drift from the printed one. */
.print-label {
	padding: 1.5cm;
	font-family: "Space Grotesk", sans-serif;
}

/* The order number under the name, small: the courier does not need it, but
   the shop does when the parcel comes back. */
.print-label-from {
	margin: 0 0 1.2cm;
	font-size: 10pt;
	letter-spacing: 0.08em;
}

.print-label-name {
	margin: 0;
	font-size: 20pt;
	font-weight: 700;
}

.print-label-line {
	margin: 0.15cm 0 0;
	font-size: 16pt;
}

/* The sample label. Marked across the top so that a sheet found on a desk a
   week later still says what it is - a label that has to be explained by the
   person who printed it is a label that will end up on a parcel. */
.print-sample-band {
	margin: 0 0 0.5cm;
	padding: 0.2cm 0.3cm;
	border: 2px solid #111;
	font-size: 12pt;
	font-weight: 700;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	text-align: center;
}

.print-sample-head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 1cm;
}

.print-sample-product {
	margin: 0;
	font-size: 12pt;
	font-weight: 600;
}

/* The barcode block at the foot, where a courier's eye goes last and a
   scanner's first. */
.print-sample-code {
	margin-top: 0.8cm;
	padding-top: 0.4cm;
	border-top: 1px solid #111;
}

.print-barcode {
	display: block;
	max-width: 100%;
	height: auto;
	fill: #000;
}

.print-sample-number {
	margin: 0.15cm 0 0.2cm;
	font-size: 12pt;
	letter-spacing: 0.22em;
	font-variant-numeric: tabular-nums;
}

/* A text button among the icons: same row, same baseline, no pill. */
.admin-order-sample {
	background: none;
	border: 0;
	padding: 0 0.2rem;
	font: inherit;
	font-size: 0.72rem;
	font-weight: 700;
	letter-spacing: 0.1em;
	color: inherit;
	opacity: 0.75;
	cursor: pointer;
}

.admin-order-sample:hover {
	opacity: 1;
	color: var(--accent);
}

/* Where the parcel goes, set apart from the buyer's own address above it -
   on a parcel these are two different places and reading them as one line is
   how a package ends up at the wrong door. Until Posti's own label is
   printed (see docs/posti-smartship.md) this is what the counter reads. */
.print-label-pickup {
	margin: 0.5cm 0 0;
	padding: 0.25cm 0.3cm;
	border: 1px solid #111;
	border-radius: 0.2cm;
	font-size: 13pt;
	line-height: 1.35;
}

.print-label-phone {
	margin: 0.6cm 0 0;
	font-size: 12pt;
}

.print-receipt {
	padding: 1.2cm;
	font-family: "Space Grotesk", sans-serif;
	font-size: 10pt;
	/* The width of the right-hand column, shared by KUITTI and the order's
	   details below it. One value in one place is what keeps the heading and
	   the block under it starting at the same letter - two separate widths
	   drift apart the moment either is touched. */
	--print-oikea: 6.5cm;
}

.print-receipt h1 {
	font-size: 17pt;
}

.print-dim {
	color: #444;
	font-size: 9pt;
	line-height: 1.5;
}

/* The seller on the left, the order's details on the right, aligned under
   KUITTI. align-items: start keeps the two blocks level at the top whichever
   of them is taller - the seller's address is four lines and the details are
   four rows, but neither is guaranteed. */
.print-meta {
	display: flex;
	justify-content: space-between;
	align-items: start;
	gap: 1cm;
	margin: 0.8cm 0;
}

/* The seller's block takes the rest of the row, so the right column's left
   edge does not move with the length of the address. */
.print-meta .print-dim {
	flex: 1 1 auto;
	margin: 0;
}

.print-head {
	margin: 0;
	display: grid;
	/* Both columns as wide as their content, and the pair pushed to the right
	   edge of the sheet: the block then ends where KUITTI and every figure
	   end, rather than starting at a left edge of its own. It can grow
	   leftwards when a company name is long - there is a whole empty column
	   beside it, and the seller's block is what sets the limit. */
	grid-template-columns: auto auto;
	justify-content: end;
	/* No row gap: the seller's address opposite is four plain lines at the
	   block's own line height, and a gap here made the same four rows stand
	   further apart than the four beside them. */
	gap: 0 0.6cm;
	/* At least the heading's column, and wider when the content needs it: the
	   address is kept on one line (below), and a fixed basis would have
	   wrapped it. The block grows leftwards into empty paper, and its right
	   edge - the one everything else lines up on - does not move. */
	flex: 0 1 auto;
	min-width: var(--print-oikea, 6.5cm);
	/* The same size as the seller's block opposite. They are the same kind of
	   thing - who sold, who bought - and setting one larger than the other
	   said there was a difference in importance that there is not. */
	font-size: 9pt;
	line-height: 1.5;
}

.print-head div,
.print-totals div {
	display: contents;
}

.print-head dt,
.print-totals dt {
	color: #444;
}

.print-head dd,
.print-totals dd {
	margin: 0;
}

/* The totals are what a reader looks for on a receipt, so they carry the
   weight. The buyer's details are reference, not emphasis - in bold they
   competed with the figures for the same attention. */
.print-totals dd {
	font-weight: 600;
}

/* The figures line up on their right edge and on their digits: the column is
   as wide as the widest number in it, so without this a shorter sum sat left
   inside the column while "-36,49" filled it. */
.print-totals dd {
	text-align: right;
	font-variant-numeric: tabular-nums;
}

/* The buyer's values end at the sheet's edge, every one of them. The column
   is as wide as the longest value, so left-aligned the short ones stopped
   wherever they happened to stop and the block had a ragged edge against a
   sheet that lines up everywhere else. */
/* One line each: an address broken over two lines reads as two facts, and
   "Kiljavantie 5, 05200" above a lonely "Rajamäki" is not an address. */
.print-head dd {
	text-align: right;
	white-space: nowrap;
}

.print-lines {
	width: 100%;
	border-collapse: collapse;
	margin-bottom: 0.6cm;
}

.print-lines th,
.print-lines td {
	padding: 0.15cm 0.2cm;
	border-bottom: 1px solid #ccc;
	text-align: left;
	vertical-align: top;
}

/* Written as td.print-num, and that is the whole point: ".print-lines td"
   weighs (0,1,1) and ".print-num" alone (0,1,0), so the table's own
   left-align beat it wherever it applied - and every figure in the product
   table sat at the left of its column while the totals underneath were
   flush right. Specificity, not order: no amount of moving the rule would
   have fixed it. */
.print-num,
.print-lines th.print-num,
.print-lines td.print-num {
	text-align: right;
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/* The last column ends where the totals end. The cells carry 0.2 cm of side
   padding, which on the outermost column pushed every line's sum that far in
   from the paper's edge while the total below it sat flush - two columns of
   figures a couple of millimetres apart, which is exactly the distance that
   reads as a mistake rather than as a margin. */
.print-lines th:last-child,
.print-lines td:last-child {
	padding-right: 0;
}

.print-totals {
	display: grid;
	grid-template-columns: 1fr auto;
	gap: 0.1cm 1cm;
	max-width: 9cm;
	margin-left: auto;
}

/* The total's rule as one line.
   The rows are display: contents, so a border on dt and on dd drew two
   segments with the column gap showing between them - a broken line across
   the page. This row steps out of contents and spans both columns, so the
   rule is one stroke. */
.print-totals .print-total {
	display: flex;
	justify-content: space-between;
	grid-column: 1 / -1;
	margin-top: 0.1cm;
	padding-top: 0.15cm;
	border-top: 1px solid #000;
	font-size: 12pt;
	font-weight: 700;
}

@media print {
	/* On paper, only the document: the shop behind it, the dimmed ground and
	   the buttons all go. */
	body.tuloste-auki > *:not(#printArea) {
		display: none !important;
	}

	#printArea {
		position: static;
		display: block;
		padding: 0;
		overflow: visible;
		background: none;
		backdrop-filter: none;
	}

	.tuloste-paperi {
		width: auto;
		max-height: none;
		overflow: visible;
		box-shadow: none;
		border-radius: 0;
	}

	.tuloste-napit {
		display: none;
	}

	/* Osoitekortti ei tulostu sivun mukana. Se on PDF, ja selaimen tulostus
	   antaisi siitä tyhjän laatikon kuitin eteen - kortti tallennetaan ja
	   tulostetaan PDF-lukijasta, jossa se tulee oikeassa koossa. */
	.tuloste-pdf {
		display: none;
	}

	/* Both documents at once: the label goes on the outside of the parcel and
	   the receipt inside it, so they cannot share a page. */
	.print-label + .print-receipt {
		break-before: page;
	}
}

/* --- Haut: mitä kauppaan kirjoitetaan --- */

/* A word that found no category. Marked rather than only sorted first: the list
   is read from the top, and the mark is what says why these are at the top. */
.admin-order-list li.admin-search-lost {
	border-color: rgba(242, 106, 27, 0.4);
}

.admin-search-term {
	font-weight: 600;
}

/* The count is a number in a column of numbers. */
/* The bike the search was made with. Beside the word rather than under it:
   the pair is the finding - "jarrupalat, Softail" is a stocking decision where
   the word alone is only a gap in the menu. */
.admin-search-bike {
	color: var(--accent);
	font-size: 0.82rem;
}

.admin-search-hits {
	opacity: 0.6;
	font-size: 0.82rem;
	font-variant-numeric: tabular-nums;
}

/* What the list is made of, said where the person reading it is. */
.admin-search-note {
	opacity: 0.6;
	font-size: 0.8rem;
}

/* How a message is answered, said once under the list rather than on every row:
   it is true of the whole panel, and repeated per row it would be noise. */
.admin-inbox-foot {
	margin: 0.6rem 0 0;
	font-size: 0.74rem;
	opacity: 0.5;
}

@media (prefers-reduced-motion: reduce) {
	.admin-inbox-bubble.admin-inbox-uusia {
		animation: none;
	}
}

/* --- Chat-kupla ------------------------------------------------------------ */
/*
   The bubble sits in the box's bottom right corner, half inside the box and half
   outside it. Its position is computed from the box, so .glass-box needs position:
   relative - which it did not have. The box has no overflow: hidden, so the half
   that hangs outside is visible.

   Visible only on the front page: .glass-box-home is a class renderFront sets
   according to the route, so the visibility comes from there rather than from logic
   of its own.
   */
/* The bubble in the view's bottom right corner, in the same place as the admin's
   inbox.

   It used to sit on the main box's right edge and only on the front page
   (.glass-box-home). Both ties have been cut: the bubble is now always in the same
   corner and always present, so the customer finds it from any view and its
   position does not depend on which box happens to be on screen.

   The same z-index as the admin's bubble: the two are never visible at once,
   because visibility comes from the role - so they do not compete. The profile box
   is 20 and stays above both. */
   /* --- A product's link into the chat ---------------------------------------- */
/*
   The button is on every product card but hidden until the chat is open. Then it
   is an offer ("attach this to your question"); at any other time it would be an
   icon whose meaning is nowhere to be found.

   In the card's top right corner on top of the image: the card has no other room
   where it would not compete with the title, the price or the add-to-cart button.
   */
.front-product-chat {
	display: none;
}

body.chat-auki .front-product-chat {
	position: absolute;
	top: 0.45rem;
	right: 0.45rem;
	z-index: 2;
	display: flex;
	align-items: center;
	/* NOT centred: the label beside the mark is a flex item of its own and it is
	   as wide as its words even while it is invisible, so centring the pair
	   pushed the mark out of the collapsed bubble. The mark is placed by its own
	   margin instead, and the label grows to the right of it on hover. */
	gap: 0;
	width: 1.9rem;
	height: 1.9rem;
	padding: 0;
	overflow: hidden;
	border: 0;
	/* The same bubble as on a phone, and as in the corner of the screen. */
	border-radius: 50% 50% 6px 50%;
	background: linear-gradient(140deg, var(--accent-mid), var(--accent-deep));
	color: #fff;
	box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
	cursor: pointer;
	transition: background 0.18s ease;
}

/* The same three lines as the question box's own bubble, sized for a bubble a
   third of its size. The element is the chat bubble's own, so the two cannot
   drift into being different marks. */
.front-product-chat .chat-bubble-lines {
	gap: 0.14rem;
	margin-left: 0.42rem;
}

.front-product-chat .chat-bubble-lines span {
	width: 0.78rem;
	height: 0.13rem;
}

.front-product-chat .chat-bubble-lines span:last-child {
	width: 0.44rem;
}

/* In the message: the lines give way to the cross that takes it back out. */
.front-product-chat-added .chat-bubble-lines {
	display: none;
}

/* The cross, which is the only thing drawn as a picture here now. */
.front-product-chat svg {
	width: 1.05rem;
	height: 1.05rem;
	flex: none;
	margin-left: 0.42rem;
	fill: currentColor;
}

/* The cross, shown in place of the message once the product is in the question:
   the same button is the way back out. Drawn rather than filled, because a
   cross is two lines. */
.front-product-chat-cross {
	display: none;
	fill: none;
	stroke: currentColor;
	stroke-width: 2.4;
	stroke-linecap: round;
}

.front-product-chat-added .front-product-chat-cross {
	display: block;
}

/* In the question: a quieter button than the one that invites the press, so a
   grid of cards shows at a glance which of them are already in the message.
   The class is doubled to outrank the phone's own rule further down, which sets
   the inviting gradient on every one of these buttons. */
/* The selector is heavier than it looks because it has to outrank two other
   rules that paint this button: the desktop one under body.chat-auki and the
   phone's own. Both set the inviting gradient on every one of these buttons. */
body .front-product-chat.front-product-chat-added {
	background: rgba(20, 18, 16, 0.88);
	box-shadow: 0 0 0 1.5px rgba(242, 106, 27, 0.9);
}

/* "Lisätty viestiin", at the point of the press.
   On a phone the panel stays closed while products are collected, so without
   this the only sign is a number in the far corner of the screen - not where
   the eye is at that moment. */
.front-product-chat-flash::after {
	content: "Lisätty viestiin";
	position: absolute;
	top: 50%;
	right: calc(100% + 0.4rem);
	transform: translateY(-50%);
	padding: 0.28rem 0.55rem;
	border-radius: 999px;
	background: rgba(20, 18, 16, 0.92);
	color: var(--paper, #f4efe6);
	font-size: 0.68rem;
	font-weight: 700;
	letter-spacing: 0.03em;
	white-space: nowrap;
	pointer-events: none;
}

/* The words, for a screen reader. On screen the shape says it: this is the
   question box's bubble with a question in it, and the same sentence written
   beside a hundred cards would be a hundred copies of one sentence. A mouse
   gets it from the button's own title. */
.front-product-chat-label {
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/* The bubble stays a bubble under the pointer.
   It used to grow into a 13.5rem bar with its words in it, and two things were
   wrong with that: the bar reached out over the card beside it, and the shape
   that carried the meaning - the question box's own bubble - stopped being a
   bubble at the moment the customer looked straight at it. The mark went with
   it, hidden behind the bar's curved end.
   The words are still there for whoever needs them: the browser's own tooltip
   for a mouse, the label for a screen reader. */
body.chat-auki .front-product-chat:hover,
body.chat-auki .front-product-chat:focus-visible {
	background: linear-gradient(140deg, var(--accent), var(--accent-mid));
}

@media (prefers-reduced-motion: reduce) {
	.front-product-chat,
	.front-product-chat-label {
		transition: none;
	}
}

/* On a phone the button is always there, chat open or not.
   On a desktop it appears when the question is open, and that works: the shop
   and the question are on the screen together, so "open it, then point at the
   part" is one view. A phone shows one thing at a time - the panel covers the
   products it is about - so the same two steps mean holding one screen in your
   head while looking at the other, and the feature goes unused. The button is
   the entry point instead: press it on the product and the question opens with
   the product already in it. */
@media (max-width: 620px) {
	.front-product-chat {
		position: absolute;
		/* Lower than the desktop button sits. The bubble is thumb-sized here, so
		   pressed against the card's top edge it read as stuck to the corner -
		   and the corner is where a hand holding the phone already is. */
		top: 0.85rem;
		right: 0.45rem;
		z-index: 2;
		display: flex;
		align-items: center;
		justify-content: center;
		/* Smaller than it was drawn, and easier to hit than it looks.
		   The bubble sits on the product's own picture, so it should take as
		   little of it as it can get away with - but a thumb covers about 44 px
		   whatever is drawn under it, and the touch area below reaches well past
		   the bubble's edge. The one carrying a cross keeps the larger size: it
		   is the one pressed in a hurry, to take something back out. */
		width: 2.2rem;
		height: 2.2rem;
		padding: 0;
		border: 0;
		/* The question box's own bubble, in miniature: round on three corners
		   with a tail at the bottom right, the same shape and the same gradient
		   as the one in the corner of the screen. */
		border-radius: 50% 50% 6px 50%;
		background: linear-gradient(140deg, var(--accent-mid), var(--accent-deep));
		color: #fff;
		box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
		cursor: pointer;
	}

	/* The mark sits in the middle of the circle, and it is most of the circle.
	   The icon is the whole message on a phone - the words beside it do not fit
	   - so it is read at arm's length rather than inspected. The margin below is
	   for the desktop button, where the icon shares its row with a label that
	   slides in; here there is no label and the margin pushed it off centre. */
	.front-product-chat svg {
		width: 1.6rem;
		height: 1.6rem;
		margin-left: 0;
	}

	.front-product-chat .chat-bubble-lines {
		gap: 0.16rem;
		margin-left: 0;
	}

	.front-product-chat .chat-bubble-lines span {
		width: 0.9rem;
		height: 0.15rem;
	}

	.front-product-chat .chat-bubble-lines span:last-child {
		width: 0.5rem;
	}

	/* In the message: the size the bubble had before it was made smaller. This
	   is the button that takes a product back out, and it is pressed while
	   scrolling past a card that is already in the question. */
	.front-product-chat.front-product-chat-added {
		width: 2.8rem;
		height: 2.8rem;
	}

	/* A touch target wider than the circle it is drawn as.
	   The button sits on top of the image, and the image is a link to the
	   product: a press that lands a few pixels outside the circle opened the
	   product page instead of putting it in the question - which is the one
	   outcome a customer collecting parts does not want. The area reaches past
	   the circle without the circle growing. */
	.front-product-chat::before {
		content: "";
		position: absolute;
		inset: -0.75rem;
		/* A square rather than a circle: the corners are where a thumb lands when
		   it misses, and a circle leaves exactly those uncovered. It is
		   transparent, so the button still reads as the circle it is drawn as. */
		border-radius: 0.35rem;
	}

}


.chat-widget {
	position: fixed;
	right: clamp(1rem, 3vw, 2.25rem);
	bottom: clamp(1rem, 3vh, 2rem);
	/* Above the login button and the profile card, both of which are 20. The
	   bubble floats and is always in the same corner, so it inevitably ends up
	   over them - and while it is open, it is the thing being used. */
	z-index: 25;
}

/* The number of products waiting in the question, on the bubble itself.
   It is the only feedback a phone gets while the panel stays closed, so it sits
   where the eye already is after pressing a product's own button. */
.chat-bubble-count {
	position: absolute;
	top: -0.15rem;
	right: -0.15rem;
	display: grid;
	place-items: center;
	min-width: 1.4rem;
	height: 1.4rem;
	padding: 0 0.32rem;
	border-radius: 999px;
	background: var(--accent);
	color: #fff;
	font-size: 0.82rem;
	font-weight: 700;
	line-height: 1;
	box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
}

/* A speech bubble: round on three corners with a tail at the bottom left - the
   shape that reads as a speech bubble without explanation.

   The global button rule gives a pill shape and padding, so both are undone. */
.chat-bubble {
	position: relative;
	width: clamp(3.6rem, 6vw, 4.6rem);
	height: clamp(3.6rem, 6vw, 4.6rem);
	padding: 0;
	display: grid;
	place-items: center;
	border: 0;
	/* The tail in the bottom right corner: the bubble is at the box's right edge and
	   its panel opens up and to the left, so the tail points away from the content
	   rather than over it. */
	border-radius: 50% 50% 6px 50%;
	background: linear-gradient(140deg, var(--accent-mid), var(--accent-deep));
	box-shadow: 0 10px 24px rgba(0, 0, 0, 0.4);
	cursor: pointer;
	/* Above the panel: the bubble is the open and close control, so it is always
	   visible and pressable. */
	position: relative;
	z-index: 2;
	transition: transform 0.14s ease, box-shadow 0.14s ease;
}

.chat-bubble:hover,
.chat-bubble:focus-visible {
	background: linear-gradient(140deg, var(--accent-mid), var(--accent-deep));
	transform: scale(1.07);
	box-shadow: 0 14px 30px rgba(0, 0, 0, 0.45);
}

/* Three dots inside the bubble: it is the chat's own mark. A question mark was
   tried, but the dots read better as a speech bubble. Three spans as markup
   rather than an icon file, so nothing more has to be loaded. */
/* Lines rather than dots, and the reason is the promise rather than the
   drawing. Three dots in a speech bubble is what WhatsApp, Messenger and
   iMessage use to say somebody is typing RIGHT NOW - and this bubble is not a
   live chat: the customer writes a question and the answer comes by email.
   Lines say "a written message", which is what this is.

   Three lines, the last one clearly shorter: that is the shape of a written
   message - full lines and the end of a line. The short last line is what keeps
   it from reading as a hamburger menu, where all three would be equal. */
.chat-bubble-lines {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 0.22rem;
}

.chat-bubble-lines span {
	width: 1.35rem;
	height: 0.2rem;
	border-radius: 2px;
	background: #fff;
	opacity: 0.92;
}

.chat-bubble-lines span:last-child {
	width: 0.78rem;
}

/* The panel opens ABOVE the bubble, aligned to its right edge: the bubble is
   already at the box's right edge, so a panel opening to the right would run off
   the screen. */
/* The panel opens ABOVE the bubble and stays anchored to it.
   
   Three attempts and three different faults, so all three conditions are written
   down here:
   
   1. The panel must not cover the bubble. Hence it sits above the bubble
   (bottom: 100% + the gap) rather than pinned to the viewport - pinned to the
   viewport it came loose from the bubble and settled on top of it.
   2. The panel must not reach up into the profile box's area. The ceiling is
   computed from the VIEWPORT rather than from the box's height: the worst case is
   the box's bottom edge at the bottom of the viewport, where the bubble's underside
   is ~7,4 rem from the bottom - so subtracting 14 rem leaves ~6,6 rem at the top,
   i.e. the height of the profile box.
   3. The fields must not be cut off. The content is sized to fit within this
   ceiling in a 700 px browser window, and the log is the part that flexes.
   
   The right edge to the middle of the bubble, i.e. onto the box's border line: the
   bubble is half outside the box, so right: 0 would push the panel past the edge as
   well, and off the screen on a narrow display. */
.chat-panel {
	position: absolute;
	/* right: 0 rather than 50%: the bubble now sits wholly in the corner, so the
	   panel's edge does not have to be pulled back as it did when the bubble hung
	   half over the box's edge. */
	right: 0;
	bottom: calc(100% + var(--bubble-gap));
	/* Widened: the panel now also holds the bike and five topic chips, and 27rem
	   broke the topic row onto three lines. */
	width: min(33rem, calc(100vw - 2.5rem));
	/* A ceiling on the space ABOVE the bubble, not on the whole viewport.
	   100vh minus a small constant gave the panel more height than there is room
	   above it, so the panel's top edge - the heading "Kysy ihmeessä!" included -
	   ran off the screen.
	   The same expressions as in .chat-widget's position and the bubble's size,
	   plus var(--bubble-gap) and 1rem of air at the top. */
	max-height: calc(100vh - clamp(1rem, 3vh, 2rem) - clamp(3.6rem, 6vw, 4.6rem)
	                 - var(--bubble-gap) - 1rem);
	/* The panel does NOT scroll itself; the message log inside it does (see
	   .chat-log). Scrolling the whole panel pushed the email field and the send
	   button off screen - and those are the reason the panel is open, so they must
	   not disappear. The vertical axis is flex, so the log is the part that flexes
	   and the fields always stay at the bottom. */
	display: flex;
	flex-direction: column;
	overflow: hidden;
	/* More padding at the bottom than at the top: the send button and the demo note
	   need room below them, otherwise the button looks glued to the panel's edge. */
	padding: 1.4rem 1.6rem 1.9rem;
	border-radius: 16px;
	border: 1px solid rgba(255, 255, 255, 0.16);
	background: #14110f;
	box-shadow: 0 18px 44px rgba(0, 0, 0, 0.5);
	color: #f4efe6;
	text-align: left;
	/* Below the bubble within this widget: the bubble is what gets pressed, and it
	   must not end up behind the panel. */
	z-index: 1;
}

/* Otsikkorivi on ikkunan vetokahva.
 *
 * The cursor says so before the user tries; without that hint being draggable
 * is a feature nobody finds. user-select is off, because dragging would
 * otherwise select the title text every time. */
.chat-title,
.chat-presence {
	cursor: move;
	user-select: none;
	-webkit-user-select: none;
}

/* A drag must not leave text selected elsewhere in the panel either, and the
   shadow says the panel has come loose from its place. */
.chat-panel-dragging {
	user-select: none;
	box-shadow: 0 26px 60px rgba(0, 0, 0, 0.6);
}

.chat-close {
	position: absolute;
	top: 0.5rem;
	right: 0.6rem;
	width: 1.9rem;
	height: 1.9rem;
	padding: 0;
	display: grid;
	place-items: center;
	border: 0;
	border-radius: 50%;
	background: none;
	color: #f4efe6;
	font-size: 1.3rem;
	line-height: 1;
	opacity: 0.6;
	cursor: pointer;
}

.chat-close:hover,
.chat-close:focus-visible {
	background: none;
	color: var(--accent);
	opacity: 1;
}

.chat-title {
	margin: 0;
	padding-right: 1.6rem;
	color: var(--accent);
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.75rem;
	letter-spacing: 0.03em;
	line-height: 1.1;
}

/* Presence in the smallest size: it is status rather than a message. */
.chat-presence {
	margin: 0.15rem 0 0;
	font-size: 0.78rem;
	opacity: 0.55;
}

/* The instruction larger than the presence row: it is the thing the user has to
   read, and it asks something of them. */
.chat-lead {
	margin: var(--chat-gap) 0 0;
	font-size: 0.94rem;
	line-height: 1.45;
}

/* The sent questions in chat style: bubbles aligned right, because they are the
   user's own messages. The left side is left for replies, should any ever arrive.

   Scrolling and a height limit, so the panel does not grow to the height of the
   screen when several questions are sent. */
.chat-log {
	display: grid;
	gap: 0.6rem;
	margin-top: var(--chat-gap);
	/* flex: 0 1 auto = take as much room as the content needs, but shrink if the
	   window does not have it. min-height: 0 is mandatory: a flex child defaults to
	   min-height: auto, which prevents shrinking below the content and would push the
	   fields through the bottom. */
	/* A min-height rather than 0: the log may shrink, but not out of sight. At zero
	   it disappeared entirely when room was tight, and the messages went with it.
	   :empty hides the block completely when there are no messages, so this floor
	   only applies when there is something to show. */
	flex: 0 1 auto;
	min-height: 3.5rem;
	max-height: 16rem;
	overflow-y: auto;
}

.chat-log:empty {
	display: none;
}

.chat-message {
	justify-self: end;
	max-width: 88%;
	padding: 0.55rem 0.7rem;
	border-radius: 12px 12px 4px 12px;
	background: linear-gradient(140deg, var(--accent-mid), var(--accent-deep));
	color: #fff;
	font-size: 0.92rem;
	line-height: 1.4;
	overflow-wrap: anywhere;
}

/* The email row is lighter than a message: it is information the user supplied
   rather than a question, so it does not compete with the message itself - a
   border is enough to say that it was sent. */
.chat-message-meta {
	background: none;
	border: 1px solid rgba(191, 58, 31, 0.5);
	color: #f4efe6;
	font-size: 0.86rem;
	overflow-wrap: anywhere;
}

/* The state below the bubble, at the right edge: the same place as in messaging
   apps. */
.chat-status {
	justify-self: end;
	margin: -0.25rem 0 0;
	font-size: 0.72rem;
	opacity: 0.5;
}

/* A message that did not go. Full strength and on its own line rather than
   dimmed at the end of the row: the line above it says the message was written,
   and this is the only thing saying it is still unsent. The customer's words
   are back in the box below, so there is something to act on. */
.chat-status-bad {
	justify-self: stretch;
	opacity: 1;
	color: var(--accent-soft);
}

/* The question's context: the user's own bike and the topic.
 *
 * BELOW the log and ABOVE the form: it belongs to what is about to be sent rather
 * than to what has already been said. */
.chat-context {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.5rem 0.55rem;
	padding: var(--chat-gap) 0 0;
}

.chat-chip-label {
	font-size: 0.7rem;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	opacity: 0.6;
	margin-right: 0.3rem;
}

/* The selected bike. Highlighted, because it is something the shop already knows
   - the customer does not have to type it, and they need to see that it is
   included. */
.chat-chip-bike {
	display: inline-flex;
	align-items: center;
	gap: 0.15rem;
	padding: 0.25rem 0.3rem 0.25rem 0.6rem;
	border: 1px solid var(--accent);
	border-radius: 999px;
	background: rgba(242, 106, 27, 0.15);
	font-size: 0.82rem;
}

.chat-chip-remove {
	width: auto;
	height: auto;
	padding: 0 0.35rem;
	border-radius: 999px;
	background: none;
	color: inherit;
	font-size: 1rem;
	line-height: 1;
	opacity: 0.7;
}

.chat-chip-remove:hover {
	opacity: 1;
	background: rgba(242, 106, 27, 0.3);
}

.chat-topics {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.3rem;
	width: 100%;
}

/* The topic is optional, so unselected chips are faint and do not invite a press.
   The selected one stands out by being filled. */
.chat-topic {
	width: auto;
	height: auto;
	padding: 0.22rem 0.6rem;
	border: 1px solid rgba(255, 255, 255, 0.25);
	border-radius: 999px;
	background: none;
	color: rgba(244, 239, 230, 0.75);
	font-size: 0.78rem;
	font-weight: 500;
}

.chat-topic:hover {
	border-color: var(--accent);
	color: #fff;
}

.chat-topic-on {
	border-color: var(--accent);
	background: var(--accent);
	color: #14100d;
	font-weight: 700;
}

/* The products attached to a question.
 *
 * A card rather than an address: a product used to be added to the text field as
 * "749947 MCJ Remote Control Motor" with the full URL below it. The customer saw
 * a long link and whoever answered got text to read. An image and a title say the
 * same at a glance - and the card is still a link. */
/* The message box: attachments and the text field as one surface.
 *
 * The border and the background move here from the field, and the field inside
 * becomes borderless. Without that the attached product sat in a box of its own
 * above another box, which read as two separate things - and a product attached
 * to a message is part of the message.
 *
 * :focus-within lights the whole box when the cursor is in the field, so the
 * focus ring belongs to the box rather than to the half of it that happens to
 * take the caret. */
.chat-compose {
	border: 1px solid rgba(255, 255, 255, 0.2);
	border-radius: 10px;
	background: rgba(255, 255, 255, 0.07);
	padding: 0.4rem;
	display: grid;
	gap: 0.4rem;
	/* What gives way when the message grows: the products already attached and
	   the lines written about them scroll inside the box, and everything around
	   it - the heading above, the address and the button below - stays where it
	   is. A message is written a piece at a time and read back while it is
	   written, so scrolling it is the one thing here that costs nothing. */
	flex: 1 1 auto;
	min-height: 5.5rem;
	overflow-y: auto;
}

.chat-compose:focus-within {
	border-color: var(--accent-mid);
}

.chat-form .chat-compose textarea {
	border: 0;
	border-radius: 6px;
	background: none;
	padding: 0.3rem 0.4rem;
}

.chat-form .chat-compose textarea:focus,
.chat-form .chat-compose textarea:focus-visible {
	outline: none;
}

.chat-attachments {
	display: grid;
	gap: 0.4rem;
}

/* Empty it takes no room: the box is a text field until something is attached. */
.chat-attachments:empty {
	display: none;
}

/* How to attach a product. Dim and small: it is an instruction, not part of the
   message, and it sits inside the box the customer is writing in. */
.chat-attach-hint {
	margin: 0;
	padding: 0 0.4rem 0.2rem;
	font-size: 0.72rem;
	line-height: 1.35;
	color: rgba(244, 239, 230, 0.55);
}

/* Gone once anything has been added: an instruction for something already done
   is noise, and the box is short of room.
   :has rather than an adjacent sibling, because the live field now sits between
   the parts and this hint - the parts come first so that a new line lands after
   the product added last. */
.chat-compose:has(.chat-attachments:not(:empty)) .chat-attach-hint {
	display: none;
}

/* A frozen line: what the customer wrote before attaching a product. Shown as
   text rather than in a second field - two editable boxes in one message would
   leave them wondering which one they are writing in. */
.chat-part-text {
	display: block;
	padding: 0.1rem 0.4rem;
	white-space: pre-wrap;
	overflow-wrap: anywhere;
	font-size: 0.92rem;
	line-height: 1.4;
}

.chat-attachments:empty {
	display: none;
}

.chat-attachments:not(:empty) {
	padding-top: var(--chat-gap);
}

.chat-product {
	display: flex;
	align-items: center;
	gap: 0.3rem;
	padding: 0.35rem 0.4rem 0.35rem 0.35rem;
	border: 1px solid rgba(255, 255, 255, 0.18);
	border-radius: 12px;
	background: rgba(255, 255, 255, 0.05);
}

.chat-product-link {
	display: flex;
	align-items: center;
	gap: 0.6rem;
	flex: 1 1 auto;
	min-width: 0;
	color: inherit;
	text-decoration: none;
}

.chat-product-image {
	width: 2.6rem;
	height: 2.6rem;
	flex: none;
	border-radius: 8px;
	object-fit: contain;
	background: #fff;
}

.chat-product-image-empty {
	background: rgba(255, 255, 255, 0.12);
}

/* min-width: 0 all the way down the chain, so a long title is cut with an
   ellipsis rather than widening the card past the panel. */
.chat-product-text {
	display: grid;
	min-width: 0;
}

.chat-product-title {
	overflow: hidden;
	white-space: nowrap;
	text-overflow: ellipsis;
	font-size: 0.85rem;
	font-weight: 600;
}

.chat-product-link:hover .chat-product-title {
	text-decoration: underline;
}

.chat-product-sku {
	font-size: 0.75rem;
	opacity: 0.65;
}

.chat-product-remove {
	width: auto;
	height: auto;
	flex: none;
	padding: 0 0.4rem;
	border-radius: 999px;
	background: none;
	color: inherit;
	font-size: 1.05rem;
	line-height: 1;
	opacity: 0.6;
}

.chat-product-remove:hover {
	opacity: 1;
	background: rgba(255, 255, 255, 0.12);
}

/* Sent products in the log: to the right edge like the message bubbles, because
   they are part of what the customer sent. */
/* The message's own text, inside the bubble.
 *
 * The bubble used to carry white-space: pre-wrap itself. It cannot any more: it
 * now also holds product cards, and their markup's every newline would show as a
 * blank line. So the property belongs to the text.
 *
 * (This rule went missing once. The edit that introduced it threw on a later
 * line of the same script, before the file was written, and nothing failed
 * afterwards - the text simply lost its line breaks.) */
.chat-message-text {
	display: block;
	white-space: pre-wrap;
	overflow-wrap: anywhere;
}

/* The attached products under the text, inside the same bubble.
 *
 * Under it because that is how the message is meant: the customer writes the
 * question and then points at the part it is about. Above the text the bubble
 * opened with a product and the question read as its caption. */
.chat-message-products {
	display: grid;
	gap: 0.35rem;
	margin: 0.45rem 0 0;
}

.chat-form {
	margin-top: var(--chat-gap);
	display: flex;
	flex-direction: column;
	gap: 0.65rem;
	/* The form takes the room that is left and gives it to the message inside
	   it. It used to be flex: none with the log as the only thing that could
	   shrink, and then five products and a few sentences pushed LÄHETÄ off the
	   bottom of the panel - measured on a phone, the button sat 15 px below the
	   panel's own edge. The button is the reason the panel is open.
	   min-height: 0 is mandatory: a flex child will not shrink below its content
	   without it, and the overflow below would never come into play. */
	flex: 1 1 auto;
	min-height: 0;
}

/* The address and the button are the panel's floor: they do not shrink and they
   do not scroll away. */
.chat-form input[type="email"],
.chat-form .chat-email-note,
.chat-form button[type="submit"] {
	flex: none;
}

/* More space above the send button than between the fields: the fields are one
   thing in sequence, the button is what ends the input. */
.chat-form button[type="submit"] {
	margin-top: 0.3rem;
	justify-self: stretch;
}

/* Rules of their own for both: the global input rule gives a light ground and
   dark text, which is the wrong way round in this dark panel. It does not match a
   textarea at all, so without this the two fields would look different. */
.chat-form textarea,
.chat-form input {
	width: 100%;
	padding: 0.7rem 0.8rem;
	border: 1px solid rgba(255, 255, 255, 0.2);
	border-radius: 10px;
	background: rgba(255, 255, 255, 0.07);
	color: #f4efe6;
	font: inherit;
	font-size: 0.92rem;
}

/* No resize handle. It showed as diagonal lines in the field's bottom right
   corner, and it is a textarea's native handle - "resize: vertical" draws it.
   Resizing would fight the panel's layout anyway: the panel has a ceiling and the
   log is the part that flexes, so a hand-grown field would squeeze the log shut.

   A rule of its own for the textarea: resize means nothing for an input, so a
   shared rule set it on both for no reason. */
.chat-form textarea {
	/* A taller input: two rows were enough for "is this in stock", but not for the
	   one that describes what is wrong with the bike - and that is the question the
	   panel gets opened for. */
	min-height: 5.5rem;
	resize: none;
}

.chat-form textarea::placeholder,
.chat-form input::placeholder {
	color: rgba(244, 239, 230, 0.45);
}

.chat-form textarea:focus,
.chat-form input:focus {
	outline: 2px solid var(--accent);
	outline-offset: 1px;
}

/* The email field's notice.
 *
 * Below the field and in red, but without a ! icon or a large box: this is a typo
 * in the middle of a form, not a failed send. */
.chat-email-note {
	margin: -0.2rem 0 0;
	font-size: 0.8rem;
	font-weight: 600;
	color: #ff9a92;
}

/* The field's border at the same time: colour alone does not say WHICH field is
   meant when there are two of them stacked. */
/* The field that is holding the send up. Orange rather than a red of its own:
   it is the shop's own colour and the one thing on the panel that draws the
   eye, and nothing here is an error in the sense a red border claims - the
   customer has simply not finished writing yet.

   On .chat-compose too, since the message field's border lives on that wrapper
   (the textarea inside it is borderless), and the mark has to appear on the box
   the customer is looking at. */
.chat-input-bad,
.chat-compose.chat-input-bad,
.bike-finder-select.chat-input-bad {
	border-color: var(--accent) !important;
}

.chat-demo {
	margin: var(--chat-gap) 0 0;
	font-size: 0.76rem;
	opacity: 0.5;
}

@media (prefers-reduced-motion: reduce) {
	.chat-bubble {
		transition: none;
	}
}

/* The background image appears only on the front page: on a category page, in
   search results and in the cart the box is full of content, where the image would
   only hurt readability. renderFront swaps the class according to the route.

   The combined selector and the position after .glass-box are both necessary:
   .glass-box sets its background with the background shorthand, which resets
   background-image, so a single-class rule lost here outright. */
/* On the front page the box is taller: the menu sits at the top and the campaign
   text at the bottom, so the space between them needs room. In the other views the
   box's height comes from its content. */
.glass-box.glass-box-home {
	min-height: clamp(320px, 56vh, 620px);
}


.front-category {
	display: block;
	color: inherit;
	text-decoration: none;
	/* transform is included in the transition: a hover grows the heading. The growth
	   is done with transform rather than font-size, because changing the font size
	   re-lays the whole menu row - the neighbours would move and at worst the hovered
	   heading would escape from under the cursor, making the effect flicker. */
	transition: color 0.12s ease, transform 0.14s ease;
	margin: 0;
	/* No letter-spacing: it would add space after the last letter too, so a centred
	   row leans left by half of that trailing space and the dropdown no longer lands
	   under the ink. Without it both texts centre on the same axis structurally,
	   without sub-pixel compensation. */
	/* A Bebas Neue glyph's ink reaches about a pixel to the left of the element's
	   box, so the repaint area of the hover colour change left the T's edge pixel in
	   the old colour. The padding brings the ink inside the box. */
	padding: 0.15rem 0.4rem;
	font-family: "Bebas Neue", sans-serif;
	line-height: 1;
	text-align: center;
}

/* The two chapters the shop leads with - oils and brakes. A step above the
   largest count-based size, because they are first for a different reason than
   the others and the row should show that. */
.front-category-lead {
	font-size: clamp(1.45rem, 2.7vw, 2rem);
	font-weight: 600;
}

.front-category-big {
	font-size: clamp(1.26rem, 2.2vw, 1.7rem);
}

.front-category-small {
	font-size: clamp(1rem, 1.5vw, 1.18rem);
	opacity: 0.8;
}

.front-category-item:hover .front-category-small {
	opacity: 1;
}


.front-category-item {
	position: relative;
	text-align: center;
}

/* When a main menu item is open, the others dim AND shrink so the open one stands
   out. This rule comes before the highlight below: the same specificity, so the
   order decides which wins. */
.front-menu:hover .front-category {
	color: rgba(244, 239, 230, 0.28);
	transform: scale(0.86);
}

/* The growth is modest because the headings are not the same length. A scale
   is proportional, so the same 22 % that moved "Jarrut" by 15 px moved "Öljyt,
   suodattimet ja öljynlauhduttimet" by 68 px - measured at 1440 px, the
   hovered oils heading ran 27 px over JARRUT and reached the box's own edge on
   the other side. At 8 % the widest heading grows 25 px each way, which the
   row's gap holds. Colour carries the rest of the emphasis. */
.front-category-item:hover .front-category {
	color: var(--accent);
	text-decoration: none;
	transform: scale(1.08);
}

/* The selected chapter stays orange and underlined regardless of what is hovered.
   Three selectors at the same specificity (0,3,0), all after the rules above, so
   they win by order:
        1. the base state
        2. the menu is hovered but something else  -> does not dim
        3. this one is hovered itself              -> does not lose the underline
   Without the third, ".front-category-item:hover .front-category" would reset the
   underline exactly when the cursor is over the selected heading. */
.front-category-active,
.front-menu:hover .front-category-active,
.front-category-item:hover .front-category-active {
	color: var(--accent);
	text-decoration: underline;
	text-decoration-thickness: 2px;
	text-underline-offset: 0.28em;
}

/* The counter without an underline: an underline inherits from the parent and
   cannot be undone in the child with a "text-decoration: none" rule -
   inline-block breaks the inheritance. */
.front-category-active .front-category-count {
	display: inline-block;
	text-decoration: none;
}

/* The selected one neither grows nor shrinks on hover: its size reports the
   chapter's size (front-category-big/small), and mixing two meanings into one
   measure would make both unclear. */
.front-menu:hover .front-category-active {
	transform: none;
}

@media (prefers-reduced-motion: reduce) {
	.front-category {
		transition: color 0.12s ease;
	}
}

.front-category,
.front-category:hover,
.front-category:active {
	-webkit-tap-highlight-color: transparent;
}

/* The subcategories open on hover below the chapter. They are in normal flow, so
   no dead zone is left for the mouse - and because the spacing is padding rather
   than margin, the hover does not break on the way down. */
.front-subcategories {
	display: none;
	position: absolute;
	top: 100%;
	/* Centred under its chapter rather than left-aligned with it. The chapters
	   are not the same width - "Öljyt, suodattimet ja öljynlauhduttimet" is
	   621 px and "Jarrut" is 137 - so aligning the panel's left edge with the
	   chapter's put the widest chapter's panel hard against the box's own
	   edge while its list sat nowhere near the words it belongs to. Centred,
	   the panel stands under its heading whichever heading it is.

	   The nudge that keeps it inside the box is a variable rather than a left
	   offset, so the browser's measurement (see app.js) can move the panel
	   without overwriting the centring. */
	left: 50%;
	right: auto;
	transform: translateX(calc(-50% + var(--siirto, 0px)));
	/* Content width. It used to be at least as wide as the chapter, which for
	   the longest heading meant a panel half again as wide as its list. */
	width: max-content;
	/* Umpinainen musta paneeli: lapikuultavan lapi nakyisi alla oleva sisalto, mika tekee
	   7 kohdan listasta lukukelvottoman. */
	margin: 0.45rem 0 0;
	padding: 0.55rem 0.7rem;
	background: #000;
	border: 1px solid rgba(255, 255, 255, 0.18);
	border-radius: 12px;
	box-shadow: 0 14px 34px rgba(0, 0, 0, 0.55);
	list-style: none;
	white-space: nowrap;
	/* The height is capped by the browser (app.js) to the box the menu sits in;
	   a list too long for that room scrolls inside the panel rather than
	   hanging out of the shop's frame. */
	overflow-y: auto;
	overscroll-behavior: contain;
	z-index: 4;
}

/* An invisible bridge across the margin: without it the mouse would lose the
   hover on its way from the chapter to the panel, because the gap belongs to
   neither element. */
.front-subcategories::before {
	content: "";
	position: absolute;
	/* Wider than the panel on both sides: the panel is centred under its
	   chapter and the pointer travels diagonally, so the bridge has to cover
	   the corners as well as the gap straight below. */
	left: -1.5rem;
	right: -1.5rem;
	top: -0.75rem;
	height: 0.75rem;
}

/* Open on hover, and kept open by a class while the grace period runs (see
   app.js). Hover alone closed the panel the moment the pointer crossed the gap
   between the chapter and its own list - a movement every hand makes. */
.front-category-item:hover .front-subcategories,
.front-category-open .front-subcategories {
	display: grid;
	/* Vasempaan linjaan paakategorian kanssa, ei keskitettyna. */
	justify-items: start;
	/* The column gap is larger than the row gap, so the two columns are told
	   apart. */
	gap: 0.2rem 1.8rem;
}

.front-subcategories li {
	font-size: 0.95rem;
	/* A tighter line height than in body text: Moottori's 38 subcategories come to 19
	   rows in two columns, and on a laptop screen there is about 560 px of vertical
	   space below the menu. */
	line-height: 1.35;
}

/* The submenus of the two chapters the shop leads with, a size up. Oils,
   filters and brakes are what people come back for - wear parts, not
   projects - so their lists are the ones read most often and from furthest
   away. The chapter heading already says this in the row above; the list
   saying it too is the same decision carried one level down. */
.front-subcategories-lead li {
	font-size: 1.08rem;
	line-height: 1.45;
}

.front-subcategories-lead .front-subcategory-big {
	font-size: 1.2rem;
}

.front-subcategories-lead .front-subcategory-small {
	font-size: 1.04rem;
	opacity: 0.85;
}

/* The same tiering as in the main menu: many products -> larger. */
.front-subcategory-big {
	font-size: 1.06rem;
	font-weight: 600;
}

.front-subcategory-small {
	font-size: 0.92rem;
	opacity: 0.78;
}

.front-subcategory-small:hover,
.front-subcategory-small:focus-visible {
	opacity: 1;
}

.front-subcategory-link {
	display: block;
	/* The same horizontal padding as .front-category, so the texts' left edges line
	   up. */
	padding: 0 0.4rem;
	color: rgba(244, 239, 230, 0.85);
	text-decoration: none;
	cursor: pointer;
}

.front-subcategory-link:hover,
.front-subcategory-link:focus-visible {
	color: #fff;
	text-decoration: underline;
	text-underline-offset: 0.2em;
}


/* The breadcrumb to the box's left edge; the grid centres the other items, so
   this one is overridden separately. */
/* The breadcrumb carries the navigation in a subcategory, so it is large - but
   smaller than the heading, so the hierarchy holds. */
.front-breadcrumb {
	justify-self: start;
	margin-top: 0.2rem;
	width: 100%;
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.5rem;
	font-family: "Bebas Neue", sans-serif;
	letter-spacing: 0.03em;
	font-size: clamp(1rem, 2.2vw, 1.4rem);
}

.front-breadcrumb-link {
	color: rgba(244, 239, 230, 0.85);
	text-decoration: none;
}

.front-breadcrumb-link:hover,
.front-breadcrumb-link:focus-visible {
	color: #fff;
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

.front-breadcrumb-sep {
	opacity: 0.45;
}

.front-subcategory-page {
	justify-self: start;
	margin: 0.9rem 0 0;
	padding: 0;
	list-style: none;
	display: grid;
	gap: 0.35rem;
	font-size: 0.95rem;
}


/* A link keeps focus after a click, and the browser's own focus ring draws as a
   pale patch against the black background. It is removed for the mouse but kept as
   a clear highlight for the keyboard - otherwise tabbing would be blind. */
.front-category:focus,
.front-subcategory-link:focus,
.front-breadcrumb-link:focus {
	outline: none;
}

.front-category:focus-visible,
.front-subcategory-link:focus-visible,
.front-breadcrumb-link:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 3px;
	border-radius: 4px;
}


/* The subcategory's heading below the breadcrumb: on the same left alignment, in
   the accent colour and clearly larger than the menu. */
.front-title {
	justify-self: start;
	margin: 0.45rem 0 0;
	color: var(--accent);
	font-family: "Bebas Neue", sans-serif;
	letter-spacing: 0.04em;
	font-size: clamp(1.4rem, 3vw, 2rem);
	line-height: 1;
}


/* The column count is locked per width class rather than auto-fill. auto-fill
   picked its columns from the window (4-7 of them), so a page size of 24 left a
   differently sized partial row on every load. There are now 5 / 3 / 2 columns, and
   a page size of 30 divides evenly by all of them. */
.front-products {
	justify-self: stretch;
	width: 100%;
	margin-top: 1.2rem;
	display: grid;
	grid-template-columns: repeat(5, minmax(0, 1fr));
	gap: 1.1rem;
}

/* One card rather than two separate boxes: the border, the background and the
   rounding are on the outer shell, and overflow: hidden clips the image's corners
   to the same curve. */
.front-product {
	/* The chat button is positioned against this (see .front-product-chat). The card
	   had no position value, so the button would have found its nearest positioned
	   ancestor somewhere further away - and settled in the wrong card's corner or at
	   the edge of the whole grid. */
	position: relative;
	display: flex;
	flex-direction: column;
	/* The grid's rows stretch to the same height, so a card's content can align to
	   the bottom. */
	height: 100%;
	border-radius: 12px;
	border: 1px solid rgba(255, 255, 255, 0.12);
	background: rgba(255, 255, 255, 0.06);
	overflow: hidden;
}

/* The text part is the same card: no border or background of its own. */
/* "EI VARASTOSSA" on top of the product image.
 *
 * On the image rather than on a text row: cards are skimmed thirty at a time, and
 * a row among the others does not stand out at that pace. The tag registers before
 * the title has been read.
 *
 * The image dims at the same time, so the difference reads even when there is
 * no time to read the label - and so in-stock products stand out in the
 * grid. */
.front-product-oos {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	padding: 0.25rem 0.4rem;
	background: rgba(179, 38, 30, 0.92);
	color: #fff;
	font-family: "Bebas Neue", sans-serif;
	font-size: 0.72rem;
	letter-spacing: 0.08em;
	text-align: center;
	pointer-events: none;
}

.front-product-image:has(.front-product-oos) img {
	opacity: 0.55;
}

.front-product-body {
	flex: 1;
	display: flex;
	flex-direction: column;
	gap: 0.25rem;
	padding: 0.6rem 0.7rem 0.7rem;
}

/* The supplier's images vary in size, so the image is given a fixed square slot
   and contain scaling - otherwise one large image would stretch the whole grid. */
/* The supplier's images are transparent PNGs made for a light ground, so chrome
   parts would be lost on a dark tile. A uniform light tile also makes the grid more
   even, because the aspect ratios range from 0.77 to 1.98. */
.front-product-image {
	aspect-ratio: 1 / 1;
	display: grid;
	place-items: center;
	padding: 0.6rem;
	background: #fffdf8;
	/* The cropped image is positioned absolutely and reaches past the tile, so the
	   tile is the positioning base and clips whatever overflows. */
	position: relative;
	overflow: hidden;
}

.front-product-image img {
	display: block;
	/* The slot is filled exactly and the content scaled inside it. max-width and
	   max-height in percentages do not resolve reliably in an aspect-ratio box, where
	   the image drew at its intrinsic 400px size and was clipped. */
	width: 100%;
	height: 100%;
	object-fit: contain;
}

/* The title is clamped to two lines, so the cards have the same structural height
   and the price lands at the same height across different rows too. */
/* An image drawn with measured crop bounds: the dimensions and the position come
   from an inline style, so only the positioning is set here. object-fit is not
   needed, because the width and the height are already in the image's own
   ratio. */
.front-image-trimmed {
	position: absolute;
	max-width: none;
	max-height: none;
	object-fit: fill;
}

.front-product-title {
	margin: 0;
	font-size: 1rem;
	line-height: 1.3;
	min-height: 2.34em;
	display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
	overflow: hidden;
}

.front-product-meta {
	margin: 0;
	font-size: 0.92rem;
	opacity: 0.65;
}

/* A card's title leads to the product page. It inherits the card's colour, so the
   card does not look like a pile of links - only the hover reveals it. */
.front-product-link {
	color: inherit;
	text-decoration: none;
}

.front-product-link:hover,
.front-product-link:focus-visible {
	color: var(--accent);
}

/* --- Tuotesivu ------------------------------------------------------------------- */

.front-detail {
	justify-self: stretch;
	width: 100%;
	margin: 0.8rem 0 0;
	display: grid;
	/* The image takes a fixed share and the details the rest: a long description does
	   not narrow the image. */
	grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
	gap: clamp(1rem, 2.5vw, 2rem);
	align-items: start;
}

.front-detail-image {
	aspect-ratio: 1 / 1;
	display: grid;
	place-items: center;
	padding: 0.8rem;
	border-radius: 12px;
	background: #fffdf8;
	position: relative;
	overflow: hidden;
}

.front-detail-image img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: contain;
}

.front-detail-body {
	display: flex;
	flex-direction: column;
	gap: 0.8rem;
	align-items: flex-start;
}

.front-detail-title {
	margin: 0;
	font-family: "Bebas Neue", sans-serif;
	font-size: clamp(1.4rem, 2.6vw, 2rem);
	line-height: 1.1;
	letter-spacing: 0.02em;
}

.front-detail-facts {
	margin: 0;
	display: grid;
	/* The label and the value in columns of their own, so the values line up. */
	grid-template-columns: auto 1fr;
	gap: 0.25rem 1rem;
	font-size: 0.96rem;
}

.front-detail-facts dt {
	opacity: 0.6;
}

.front-detail-facts dd {
	margin: 0;
	font-variant-numeric: tabular-nums;
}

/* Availability on the product page. Red and bold - colour alone does not reach
   someone who cannot tell it apart, and this is the fact that may call off the
   purchase. */
.front-detail-oos {
	color: #ff6b61;
}

.front-detail-price {
	margin: 0;
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.6rem;
	letter-spacing: 0.03em;
	color: var(--accent);
}

.front-site-back {
	color: var(--accent);
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.05rem;
	letter-spacing: 0.04em;
}

.front-detail-block {
	justify-self: stretch;
	width: 100%;
	margin: 1.4rem 0 0;
}

.front-detail-heading {
	margin: 0 0 0.5rem;
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.1rem;
	letter-spacing: 0.08em;
	opacity: 0.7;
}

/* The language beside the heading, smaller and fainter: it is a note about the
   text rather than part of what the section is called. */
.front-detail-lang {
	font-size: 0.85em;
	opacity: 0.7;
}

/* The part's own line, above the family's story. Set larger and lighter than
   the long text: it is the sentence that answers "what is this exactly", and
   the paragraph under it is background. */
.front-detail-lead {
	margin: 0 0 0.7rem;
	font-size: 1.05rem;
	line-height: 1.45;
	color: var(--accent-pale);
}

.front-detail-text {
	margin: 0;
	/* About 65 characters per line: the longest description is 5 205 characters, so
	   the line length decides the readability. */
	max-width: 68ch;
	line-height: 1.6;
	white-space: pre-line;
}

.front-fit-list {
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
	gap: 0.3rem 1.5rem;
	font-size: 0.9rem;
}

.front-fit-list li {
	display: flex;
	justify-content: space-between;
	gap: 1rem;
	padding: 0.2rem 0;
	border-bottom: 1px solid rgba(255, 255, 255, 0.1);
	/* A grid item will not be narrower than its widest unbreakable thing, and
	   the row that says "vuosi ei tiedossa - varmista sopivuus" was exactly
	   that: the row grew past its column and over the one beside it. */
	min-width: 0;
}

/* The family keeps its own line: a long one wraps rather than pushing the
   years off the row. */
.front-fit-family {
	min-width: 0;
}

.front-fit-years {
	opacity: 0.7;
	font-variant-numeric: tabular-nums;
	/* A year range is one thing: "1999-" on one line and "2006" on the next is
	   a different number. */
	white-space: nowrap;
}

/* The yearless row is a sentence rather than a number, so it wraps like one.
   Right aligned, so the rows still read as two columns. */
.front-fit-note {
	white-space: normal;
	text-align: right;
	min-width: 0;
}

.front-product-price {
	/* Pushes the price and the button to the bottom of the card, so they line up
	   across all of them. */
	margin: auto 0 0;
	font-family: "Bebas Neue", sans-serif;
	letter-spacing: 0.04em;
	font-size: 1.15rem;
	color: var(--accent);
}

/* The way out of an empty list, on its own line under the sentence that
   explains it. The links are the shop's own chips in miniature: the same words
   and the same counts the filter row carries. */
.front-empty-also {
	display: block;
	margin-top: 0.7rem;
	font-size: 0.95rem;
}

.front-empty-link {
	color: var(--accent);
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.1rem;
	letter-spacing: 0.04em;
	text-decoration: underline;
	text-underline-offset: 0.25em;
	white-space: nowrap;
}

.front-empty-link:hover,
.front-empty-link:focus-visible {
	color: var(--accent-mid);
}

.front-empty {
	justify-self: start;
	margin: 1rem 0 0;
	opacity: 0.7;
	/* The whole row of the product grid, not one card's column. The message is a
	   sentence and the grid's columns are 200 px wide, so in one column it
	   broke into four lines and the offer under it into two more. */
	grid-column: 1 / -1;
	max-width: 52rem;
}


.front-filters {
	justify-self: start;
	margin: 0.5rem 0 0;
	font-size: 0.96rem;
	opacity: 0.8;
}



/* Tuotemaara vasemmalle ja jarjestysvalinnat samalle korkeudelle oikeaan reunaan. */
/* Taking a whole list off the shelf, directly above the list it acts on.
 *
 * The same eye the tiles carry, in the same round button: it is the same act on
 * a different scale, and a second symbol for it would have to be learned. On
 * the right, where the grid's own controls are, and empty for a customer -
 * piirraJoukkoPiilotus draws it for the admin only. */
.front-bulk-row {
	justify-self: stretch;
	width: 100%;
	display: flex;
	justify-content: flex-end;
	gap: 0.5rem;
	min-height: 0;
}

.front-bulk-row:empty {
	display: none;
}

.front-bulk-btn {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.1rem;
	height: 2.1rem;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: rgba(4, 10, 24, 0.78);
	color: #fff;
	cursor: pointer;
}

.front-bulk-btn:hover,
.front-bulk-btn:focus-visible {
	background: var(--accent);
}

/* A press that would change nothing.
 *
 * Everything in the list is already hidden, or already on sale. The button
 * stays where it is rather than disappearing: a row whose buttons come and go
 * is a row whose other button moves under the cursor, and the pair is what
 * says which two things are possible here at all. Its title says which case it
 * is. */
.front-bulk-btn[disabled] {
	opacity: 0.35;
	cursor: default;
}

/* And it does not light up under the cursor, because it does nothing. */
.front-bulk-btn[disabled]:hover,
.front-bulk-btn[disabled]:focus-visible {
	background: rgba(4, 10, 24, 0.78);
}

.front-bulk-btn svg {
	width: 1.2rem;
	height: 1.2rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.7;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.front-count-row {
	justify-self: stretch;
	width: 100%;
	margin: 0.6rem 0 0;
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 1rem;
	flex-wrap: wrap;
}

.front-count {
	justify-self: start;
	margin: 0;
	font-size: 0.96rem;
	opacity: 0.65;
}

.front-count-row .front-sort {
	margin-left: auto;
}

.front-more {
	justify-self: center;
	margin-top: 1.1rem;
}

.front-more-btn {
	font-size: 0.9rem;
	padding: 0.6rem 2.4rem;
}

.front-more-btn[disabled] {
	opacity: 0.5;
	cursor: default;
}


.front-cart-btn {
	margin-top: 0.5rem;
	width: 100%;
	font-size: 0.94rem;
	padding: 0.5rem 0.6rem;
	letter-spacing: 0.02em;
}


/* Otsikko ja mallirajaus samalla rivilla; rajaus kayttaa jaljelle jaavan tilan. */
.front-head {
	justify-self: stretch;
	width: 100%;
	margin: 0.45rem 0 0;
	display: flex;
	align-items: baseline;
	gap: 1.5rem;
	flex-wrap: wrap;
	/* A flex item is not narrower than its widest unbreakable thing unless it
	   is told it may be, and the filters below are full of long makes. Without
	   this one of them widened the head, and the heading and the breadcrumb
	   went out over the edge of the box with it. */
	min-width: 0;
}

.front-head .front-title {
	margin: 0;
	flex: 0 0 auto;
}

/* A fixed column at the right edge: margin-left: auto always pushes it to the same
   spot, so its position does not depend on the heading's length. Otherwise "Akselit
   ja kiinnitystarvikkeet" would put the filters somewhere else than "Hihnat" does. */
.front-head-right {
	flex: 0 0 clamp(300px, 56%, 760px);
	margin-left: auto;
	display: grid;
	gap: 0.35rem;
	min-width: 0;
}

.front-sort {
	margin: 0;
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.2rem 1rem;
}

.front-sort-link {
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.05rem;
	letter-spacing: 0.03em;
	color: inherit;
	text-decoration: none;
	white-space: nowrap;
}

.front-sort-link:hover,
.front-sort-link:focus-visible {
	color: var(--accent);
}

.front-filter {
	display: flex;
	align-items: baseline;
	gap: 0.8rem;
	min-width: 0;
}

.front-filter-label {
	flex: 0 0 auto;
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.2rem;
	letter-spacing: 0.08em;
	opacity: 0.6;
}

.front-model-rows {
	display: grid;
	gap: 0.3rem;
	min-width: 0;
}

.front-model-row {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.2rem 0.9rem;
	min-width: 0;
}

/* The large ones first: models with many products. The smaller ones on a row of
   their own, in a smaller size. */
.front-model-row-big .front-model {
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.4rem;
	letter-spacing: 0.03em;
}

.front-model-row-small .front-model {
	font-size: 0.92rem;
	opacity: 0.75;
}

/* A make stays on one line - "Arlen Ness" split over two would read as two
   makes. The exception is a make too long for the line it is on: see the
   phone block, where "KB Keith Black Performance Motorcycle Pistons" was 395
   px wide in a 330 px panel and dragged the heading, the breadcrumb and the
   year row out over the edge of the box with it. */
/* Three states, one rule to read them by.
   Pale orange: this set is in the list, but nobody chose it - which is what
   every chip looks like before anything is pressed, because before anything is
   pressed everything is in the list.
   Full orange and underlined (.front-model-active): chosen.
   Grey (.front-model-out): not in the list.
   All orange and all underlined was the alternative, and it was unreadable:
   with everything lit there was nothing for a choice to stand out from. */
.front-model {
	color: var(--accent-pale);
	text-decoration: none;
	white-space: nowrap;
	max-width: 100%;
}

.front-model-out {
	color: rgba(244, 239, 230, 0.5);
}

.front-model:hover,
.front-model:focus-visible {
	color: var(--accent);
}

.front-model-active {
	color: var(--accent);
	text-decoration: underline;
	text-underline-offset: 0.22em;
}

/* The only make on the page: selected, and not a control. The cursor says so
   too - a pointer over something that cannot be pressed is a small lie. */
.front-model-only {
	cursor: default;
}

/* Matches the search: orange without an underline. The underline is reserved for
   the selection, so the two states are told apart - orange says "this fits your
   bike", the underline says "you chose this". */
.front-filter-match {
	color: var(--accent);
}

/* "Muut" is where the list of makes ends, not a make in it: the products whose
   maker the supplier did not name. A wider gap before it says so without a
   heading. */
.front-model.front-model-other {
	margin-left: 1.4rem;
}

.front-model-count {
	margin-left: 0.28rem;
	font-family: "Space Grotesk", sans-serif;
	font-size: 0.78em;
	opacity: 0.55;
}


/* Vali valitun mallin ja vuosivalintojen valiin samalla rivilla. */
.front-filter-gap {
	display: inline-block;
	width: 1.4rem;
}

/* Years are digits, and Space Grotesk renders them more clearly than Bebas. */
.front-model-row-big .front-year {
	font-family: "Space Grotesk", sans-serif;
	font-size: 1rem;
	letter-spacing: 0;
	opacity: 0.8;
}

.front-model-row-big .front-year.front-model-active {
	opacity: 1;
}




/* The chapters onto a horizontal row. Each item holds an absolutely positioned
   submenu, so the flex row does not affect how it opens. */
.front-menu {
	justify-self: center;
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: baseline;
	/* The column gap is what a hovered heading grows into, so it is wider than
	   the row gap by more than looks alone would ask for. */
	gap: 0.4rem 2.6rem;
}

.front-category-count {
	margin-left: 0.4rem;
	font-family: "Space Grotesk", sans-serif;
	font-size: 0.38em;
	font-weight: 500;
	letter-spacing: 0;
	opacity: 0.5;
}


/* The chapter's own number beside its name, in the heading's own size relation:
   smaller and quieter than the name, like every other count in the shop. */
.front-title-count {
	margin-left: 0.5rem;
	font-size: 0.55em;
	font-weight: 400;
	opacity: 0.55;
	font-variant-numeric: tabular-nums;
}

/* A section with nothing for the chosen bike. Greyed rather than dropped: a
   list that loses rows while you look at it reads as a fault, and the number
   zero is an answer. */
.front-subcategory-link.front-subcategory-empty {
	opacity: 0.45;
}

.front-subcategory-count {
	margin-left: 0.4rem;
	font-size: 0.72em;
	opacity: 0.5;
}


.brand-row {
	position: relative;
	display: flex;
	align-items: center;
	gap: 0.9rem;
	flex-wrap: wrap;
	/* The logo is out of the flow, so the row's height has to be stated or the
	   search climbs into the corner the logo fills. The wordmark sits at the
	   bottom of it, which is where the air above the search box comes from - a
	   line and a half rather than three, close enough to belong to the search and
	   far enough not to read as its caption. */
	min-height: 3.4rem;
	align-items: stretch;
	padding-bottom: 0;
}

/* ADMIN is four times the height of the shop's name, so the row it sits in has
   to grow with it - otherwise the word runs up over the logo. */
.brand-row:has(.brand-wordmark-admin) {
	min-height: clamp(3.8rem, 7vw, 5rem);
}

/* The logo, big and behind everything.
   
   It is a PNG with a transparent ground in the shop's own accent
   (scripts/build-logo.js), so it sits on the header photograph rather than in a
   white box on it - which is what lets it be this large: at this size an opaque
   mark would be a wall, and a transparent one is the wall itself.
   
   Pinned to the window's left edge rather than to the content column, so it
   reads as part of the page's ground. Taken out of the flow (absolute) so the
   search and the menu keep the places they had and simply pass in front of it,
   and put behind them with z-index - the header's own stacking context is
   .topbar (z-index: 3), and this sits at the bottom of it. */
.brand-logo {
	position: absolute;
	top: 0.6rem;
	left: calc(50% - 50vw + 0.5rem);
	z-index: -1;
	display: block;
	width: auto;
	height: clamp(9.5rem, 17vw, 14.5rem);
	/* Faint enough to pass behind text without fighting it, strong enough to be
	   the first thing the eye lands on in an empty corner. */
	opacity: 0.55;
	/* It IS the shop's name, so pressing it goes home - which needs the clicks
	   it used to refuse. Behind the content still (z-index: -1): where something
	   is drawn over it that thing answers, and everywhere else the logo does.
	   The search row is a layout box with nothing of its own to press, so it
	   passes its clicks through too and leaves the logo most of its corner. */
	pointer-events: auto;
}

/* On a phone it stays behind the search and stays large - the corner is the
   only place a shop gets to say who it is, and a small mark beside a name says
   it twice and neither of them loudly. Fainter, because the search box covers
   more of it there. */
@media (max-width: 52rem) {
	.brand-logo {
		top: -0.2rem;
		height: 4.4rem;
		opacity: 0.5;
	}

	/* No room to stand the group in on a phone: the search box already fills the
	   width. */
	:root {
		--brand-group-shift: 0rem;
	}
}

/* The shop name leads to the front page - the logo and the name in words alike,
   because both are the shop's name and a name in a corner is where people press
   to get back. It inherits the heading's style so it does not look like a link;
   only the hover reveals that it is one. */
.brand-home-link {
	color: inherit;
	text-decoration: none;
}

.brand-home-link:hover,
.brand-home-link:focus-visible {
	color: var(--accent);
}

/* How many parts are on offer, beside the shop's name. Small and faint on
   purpose: it is a fact about the catalogue, not an offer, and a customer who
   came to find one oil filter is not shopping for 37 850 of anything. The
   dealer reads the same number as a working figure in the admin panel, where
   it is given room. */
/* The shop's name in words. The heading face, so it belongs to the same voice
   as the section titles, but small and grey: the logo behind it is what carries
   the corner, and two loud things in one corner is one too many. */
.brand-wordmark {
	/* Over the search box and ending where it ends: the search is 460 px wide and
	   starts at the content column's edge, so the label runs to the same right
	   edge and reads as belonging to it rather than floating above the page.
	   Pushed to the bottom of its row, which leaves more than a line of air
	   between the words and the field - closer than that and it reads as a
	   caption on the field, and this is a label on the shop. */
	/* Out of the flow, so the heading beside it - an empty span in the shop, the
	   word ADMIN in the back office - cannot shift it: in the flow it started
	   fourteen pixels late and the right edges no longer met. */
	position: absolute;
	left: var(--brand-group-shift);
	bottom: 0;
	margin: 0;
	width: min(460px, 100%);
	/* Starting on the same vertical line as the search box and the bike button
	   below it. The three are one group, and a group has one left edge. */
	text-align: left;
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.15rem;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: rgba(244, 239, 230, 0.55);
}

/* The back office says so at the size the shop's name used to be drawn at, and
   in the shop's orange. Small and grey it was a word beside a logo that nobody
   read, and which of the two surfaces is open is the one thing this corner has
   to be unambiguous about. */
.brand-wordmark-admin {
	font-size: clamp(2.6rem, 6vw, 4.2rem);
	letter-spacing: 0.04em;
	color: var(--accent);
	line-height: 1;
}

.brand-count {
	font-family: "Space Grotesk", sans-serif;
	font-size: 0.72rem;
	letter-spacing: 0.04em;
	color: rgba(244, 239, 230, 0.32);
	/* A wider gap than the row's own: the count is a different kind of fact from
	   the name, and side by side at the same distance they read as one line. */
	margin-left: 1.6rem;
	text-transform: none;
}

/* The name carries the corner on a phone, not the logo.

   On a wide screen the logo is 230 px of orange and the name is a quiet grey
   label beside it. On a phone the logo has to be small enough to stay out of
   the search box, and at that size a round mark with its name around the rim
   says nothing - so the roles swap: the mark shrinks to a token and the name is
   the thing that is read, in the shop's own orange, hard against the search
   box's right edge.

   Below the rules it overrides rather than above them: a media query adds no
   specificity, so written first it lost to the very rules it was meant to
   change. */
@media (max-width: 52rem) {
	.brand-wordmark {
		/* On a phone it goes to the other end: the search box fills the width, so
		   the name reads as its heading either way, and at the right it does not
		   sit on the logo. */
		text-align: right;
		font-size: 1.45rem;
		letter-spacing: 0.1em;
		color: var(--accent);
	}

	/* The count stays what it is: a fact about the catalogue, not part of the
	   name, and in the name's colour and size it would read as part of it. */
	.brand-count {
		margin-left: 0.9rem;
		color: rgba(244, 239, 230, 0.4);
	}
}


/* The row for the two sets that are not in the list until they are asked for.
   Set apart from the model above it rather than from each other: the model and
   its year are the question, and these two are what can be added to the answer.
   The gap between the two of them only has to be wider than the ordinary space
   between chips (0.9rem), so they do not read as one run of words - the space
   above the row is what separates them from the question. */
.front-extra-row {
	align-items: baseline;
	/* Row gap first, then column gap. They are only apart sideways: on a phone
	   the two do not fit on one line and wrap, and a 1.5rem gap then became a
	   1.5rem hole between two lines that belong together. The sideways distance
	   is what keeps them from reading as one run of words, and on a wrapped
	   line there is nothing beside them to run into. */
	gap: 0.15rem 1.5rem;
	margin-top: 0.45rem;
}

/* The narrower of the two, set smaller. UNIVERSAALI is the larger set and the
   question more people have; this one is the tail of it. */
.front-year-extra {
	font-size: 0.82em;
}

/* The year picker, beside the model it narrows.
   A select rather than a row of buttons: the years are a hundred-odd numbers
   in a known order, and a hundred buttons is not a choice - it is a wall.
   Set apart from the model by a wider gap than the chips have between them,
   so the model and the year read as two things rather than one list. The
   picker names itself on its first line, so there is no label beside it. */
.front-year-pick {
	display: inline-flex;
	align-items: baseline;
	gap: 0.5rem;
	margin-left: 1.6rem;
}

/* Flat, like everything else in the shop.
   appearance: none removes the operating system's own control - the bevel,
   the gradient and the inset shadow that made it the one raised object on a
   page of flat surfaces. The arrow is drawn back in as a background mark,
   because without appearance there is none, and a select that does not say it
   opens is a text box that ignores typing. */
.front-year-select {
	appearance: none;
	/* The box is as wide as its widest option, so a chosen "2001" sat in a
	   field cut for "Vuosimalli" with a hand's width of nothing after it - the
	   number read as floating rather than as the answer. field-sizing lets the
	   control shrink to what it is showing. Where it is not supported the old
	   behaviour remains, which is the wide box rather than a broken one. */
	field-sizing: content;
	min-width: 6.5rem;
	font: inherit;
	font-size: 1.05rem;
	padding: 0.3rem 2.1rem 0.3rem 0.7rem;
	border: 1px solid rgba(244, 239, 230, 0.35);
	border-radius: 4px;
	background-color: transparent;
	background-image:
		linear-gradient(45deg, transparent 50%, currentColor 50%),
		linear-gradient(135deg, currentColor 50%, transparent 50%);
	background-position: right 1.05rem center, right 0.75rem center;
	background-size: 0.32rem 0.32rem, 0.32rem 0.32rem;
	background-repeat: no-repeat;
	color: inherit;
	cursor: pointer;
}

.front-year-select:hover {
	border-color: var(--accent);
	color: var(--accent);
}

.front-year-select:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

/* The option list is drawn by the browser, and a dark page needs to say so or
   the list comes out white with white text on some systems. */
.front-year-select option {
	background: #1b1b1b;
	color: #f4efe6;
}

/* The note under the picker: what the answer includes, not another question. */
.front-year-note {
	font-size: 0.85rem;
	opacity: 0.6;
}



/* A non-clickable year: the same look as the others, but no pointer and no hover
   highlight - otherwise it would look like a link that does nothing. */
.front-year-static,
.front-year-static:hover {
	color: rgba(244, 239, 230, 0.7);
	cursor: default;
}

/* Unless it is one of the bike's own years. Two single-class selectors, and
   this one is written after, so the accent wins - which is the whole point of
   the row when a bike is chosen: these are the years it matched. */
.front-year-static.front-filter-match,
.front-year-static.front-filter-match:hover {
	color: var(--accent);
}


/* The page as it is written for a search engine, and for the moment before
   the script runs. Plain and readable rather than styled to match the shop:
   what it needs is a heading, a price and words, and it is replaced by the
   application as soon as that arrives. */
.seo-page {
	padding: 1.5rem 0;
	max-width: 44rem;
}

.seo-page h1 {
	margin: 0 0 0.5rem;
	font-size: clamp(1.4rem, 3vw, 2rem);
}

.seo-breadcrumb {
	font-size: 0.85rem;
	opacity: 0.7;
	margin-bottom: 0.8rem;
}

.seo-code,
.seo-price {
	margin: 0.2rem 0;
	font-variant-numeric: tabular-nums;
}

.seo-price {
	font-weight: 700;
}

.seo-text {
	margin: 0.8rem 0;
	line-height: 1.6;
	opacity: 0.85;
}

.seo-list {
	margin: 0.8rem 0;
	padding-left: 1.1rem;
	line-height: 1.8;
}

/* --- Ostoskori --- */
.cart-link {
	/* The placement comes from the .finder-cart-row row, so there are no margins of
	   its own - except this one: on the line it wraps onto, it is the only thing
	   there, and auto keeps it at the right edge rather than the left. */
	margin: 0 0 0 auto;
	/* The negative margin lifts the cart into .topbar's area, and the topbar is
	   position: relative; z-index: 3. Without a stacking context of its own the topbar
	   would draw over the cart and swallow the clicks - the link would be visible but
	   dead, because the topbar's background is transparent. */
	position: relative;
	z-index: 4;
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	padding: 0.4rem 1rem;
	border-radius: 999px;
	border: 1px solid rgba(255, 255, 255, 0.28);
	color: #fff;
	text-decoration: none;
	font-family: "Bebas Neue", sans-serif;
	letter-spacing: 0.04em;
	font-size: 1.25rem;
}

/* An empty cart is dimmer but visible: the user sees where it is and that it is
   empty, without it competing for attention with a full one. */
/* The added state. Green, because it is a confirmation rather than a second call
   to action - an orange button only swapped its text and the change went
   unnoticed. */
.front-cart-btn-added {
	background: linear-gradient(140deg, #3f9c46, #2b6e31) !important;
}

/* A pulse on the cart icon says where the product went. Short and one-off: a
   moving icon is a call for attention, not decoration. */
@keyframes cart-pulse {
	0%   { transform: scale(1); }
	35%  { transform: scale(1.18); }
	100% { transform: scale(1); }
}

.cart-link-pulse {
	animation: cart-pulse 420ms ease-out;
}

/* Animations off if the user has asked for that. The confirmation does not
   disappear: the button's text and colour say the same without movement. */
@media (prefers-reduced-motion: reduce) {
	.cart-link-pulse {
		animation: none;
	}
}

.cart-link-empty {
	opacity: 0.55;
}

.cart-link-empty:hover,
.cart-link-empty:focus-visible {
	opacity: 1;
}

.cart-link:hover,
.cart-link:focus-visible {
	border-color: #fff;
	background: rgba(255, 255, 255, 0.1);
	color: #fff;
}

.cart-icon {
	width: 1.35rem;
	height: 1.35rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.8;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.cart-rows {
	justify-self: stretch;
	width: 100%;
	margin-top: 1rem;
	display: grid;
	gap: 0.6rem;
}

/* The make on a cart row.
 *
 * Emphasised, because it is part of what the customer bought rather than extra
 * detail - "Wurth 239211" is the thing; the number alone is a reference. The
 * wholesaler used to stand here, which named the wholesaler on a Würth fuse. */
.cart-brand {
	font-weight: 700;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--accent);
}

.cart-row {
	display: grid;
	grid-template-columns: 64px minmax(0, 1fr) auto auto auto;
	align-items: center;
	gap: 0.9rem;
	padding: 0.6rem;
	border-radius: 12px;
	border: 1px solid rgba(255, 255, 255, 0.12);
	background: rgba(255, 255, 255, 0.05);
}

/* The image is a link to the product page: it is the row's largest target and gets
   clicked instinctively. display: block, because an <a> is inline by default and
   would not honour the aspect-ratio. */
.cart-image {
	display: block;
	text-decoration: none;
	width: 64px;
	aspect-ratio: 1 / 1;
	border-radius: 8px;
	background: #fffdf8;
	overflow: hidden;
	/* The positioning base for a cropped image, see .front-image-trimmed. */
	position: relative;
}

.cart-image img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: contain;
}

/* The title is a link to the product page, but without an underline: in the cart
   and the summary every row is a product, so an underline would repeat on every
   row and distinguish nothing. Hover and keyboard focus change the colour, and
   that is enough - the same marking as on the admin text links. */
.cart-title {
	display: block;
	margin: 0;
	font-size: 0.92rem;
	color: inherit;
	text-decoration: none;
}

.cart-title:hover,
.cart-title:focus-visible {
	color: var(--accent-soft);
}

.cart-meta {
	margin: 0.15rem 0 0;
	font-size: 0.92rem;
	opacity: 0.6;
}

.cart-qty {
	display: flex;
	align-items: center;
	gap: 0.5rem;
}

.cart-step {
	width: 1.7rem;
	height: 1.7rem;
	padding: 0;
	border-radius: 999px;
	font-size: 1rem;
	line-height: 1;
}

.cart-qty-value {
	min-width: 1.6rem;
	text-align: center;
	font-variant-numeric: tabular-nums;
}

.cart-line {
	margin: 0;
	min-width: 6rem;
	text-align: right;
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.15rem;
	letter-spacing: 0.03em;
	color: #fff;
	font-variant-numeric: tabular-nums;
}

.cart-remove {
	background: none;
	border: 0;
	padding: 0.2rem 0.4rem;
	color: rgba(244, 239, 230, 0.5);
	font-size: 1.2rem;
	line-height: 1;
	cursor: pointer;
}

.cart-remove:hover {
	color: var(--accent);
}

.cart-total {
	justify-self: stretch;
	width: 100%;
	margin-top: 1rem;
	padding-top: 0.9rem;
	border-top: 1px solid rgba(255, 255, 255, 0.14);
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	font-family: "Bebas Neue", sans-serif;
	font-size: clamp(1.2rem, 2.4vw, 1.7rem);
	letter-spacing: 0.03em;
}

.cart-total strong {
	color: #fff;
	font-weight: 400;
	font-variant-numeric: tabular-nums;
}

.cart-missing {
	justify-self: start;
	margin: 0.5rem 0 0;
	font-size: 0.94rem;
	opacity: 0.6;
}

/* Tyhjennä to the left, KASSALLE to the right - i.e. directly below the total,
   because .cart-total is the same width and its figure sits at the right
   edge. */
.cart-actions {
	justify-self: stretch;
	width: 100%;
	margin-top: 1rem;
	display: flex;
	flex-wrap: wrap;
	justify-content: space-between;
	align-items: center;
	gap: 0.8rem;
}

.cart-actions button {
	padding: 0.72rem 2.4rem;
}


/* The supplier selector in the box's top right corner.
 *
 * Positioned absolutely, because it does not belong to the panel's content flow:
 * it scopes the whole box rather than being part of one view. .glass-box already
 * has position: relative. */
.front-suppliers {
	position: absolute;
	top: 0.55rem;
	right: 0.75rem;
	z-index: 3;
	display: flex;
	flex-wrap: wrap;
	justify-content: flex-end;
	gap: 0.3rem;
}

.front-supplier {
	padding: 0.2rem 0.55rem;
	border: 1px solid rgba(255, 255, 255, 0.28);
	border-radius: 999px;
	background: rgba(10, 10, 10, 0.45);
	color: rgba(244, 239, 230, 0.8);
	font-family: "Bebas Neue", sans-serif;
	font-size: 0.72rem;
	letter-spacing: 0.06em;
	line-height: 1.5;
	cursor: pointer;
	width: auto;
	height: auto;
}

.front-supplier:hover:not(:disabled) {
	border-color: var(--accent);
	color: #fff;
}

.front-supplier-on {
	border-color: var(--accent);
	background: var(--accent);
	color: #14100d;
	font-weight: 700;
}

/* The selector takes the right half of the box's top edge, so the panel's content
   starts below it. Without this the body text would run under the buttons on a
   narrow screen. */
.front-suppliers:not(.hidden) ~ .front-panel {
	padding-top: 1.6rem;
}

.front-panel {
	display: grid;
	/* Horizontally centred, but starting from the box's top edge. */
	place-items: start center;
	align-content: start;
	gap: 0.6rem;
}

/* On the front page the panel fills the box's height and pushes its content to the
   ends: the menu to the top, the campaign text to the bottom. align-content: start
   would pack both upwards, leaving the campaign stuck to the menu. */
.glass-box-home .front-panel {
	min-height: clamp(280px, 50vh, 560px);
	align-content: space-between;
}

/* The chapter page's list of options. Separate from .front-subcategories, which is
   the menu's hover panel (position: absolute, display: none) - this is the page's
   own content. The link classes are the same, so the size varies with the product
   count as it does in the menu. */
/* "Muut" sits at the end of the list and in the accent colour. Dimmed it went
   unnoticed, even though it is the only way to 946 products that have no
   subcategory - the colour tells it apart from the supplier's categories without
   letting it vanish. */
.front-others-link {
	color: var(--accent);
}

.front-chapter-list {
	justify-self: stretch;
	width: 100%;
	margin: 0.8rem 0 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem 1.6rem;
}

/* The campaigns on one row. Three separate paragraphs were three large headings
   below the picks, and they take more room than the picks themselves.

   flex + wrap rather than nowrap: three Finnish sentences do not fit a phone's
   width, and a wrapping row is better than one that continues off screen. */
.front-promo-row {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	justify-content: center;
	gap: 0.5rem 0.9rem;
}

.front-promo {
	margin: 0;
	color: var(--accent);
	font-family: "Bebas Neue", sans-serif;
	/* Smaller than when it stands alone: three sentences side by side at 1,7 rem
	   would fill half the front page. */
	font-size: clamp(1rem, 1.5vw, 1.25rem);
	letter-spacing: 0.05em;
	text-align: center;
}

/* The middot dimmed: it separates the campaigns without being their equal. */
.front-promo-sep {
	color: var(--accent);
	opacity: 0.45;
}


/* Korin otsikko valkoisella; kategoriaotsikot pysyvat korostusvarissa. */
#cartView .front-title {
	color: #fff;
}


/* The search and the cart on the same row: the cart does not claim a row of its
   own above the box, so the box rises directly below the search. */
.search-row {
	width: 100%;
	/* A container, not a control: its clicks go to whatever is under it, which
	   in the corner is the logo. Its own children take theirs back below. */
	pointer-events: none;
	padding-left: var(--brand-group-shift);
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1.5rem;
	flex-wrap: wrap;
	/* The gap to the line above it. The menu is on this row rather than between
	   the name and the search, so the whole gap comes from this one value - and it
	   is now a short one, because what sits above is the shop's name in small
	   grey capitals and it belongs to the search box under it. */
	margin-top: clamp(0.6rem, 1.2vh, 1rem);
}


/* The "Katso myös" hint: universal parts fall outside the model filter, so they
   are offered separately on a row of their own. */
.front-also {
	margin-top: 0.15rem;
}

.front-also-label {
	font-family: "Bebas Neue", sans-serif;
	font-size: 1rem;
	letter-spacing: 0.06em;
	opacity: 0.5;
}

.front-also-link {
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.05rem;
	letter-spacing: 0.03em;
}


.front-also-search {
	justify-self: start;
	margin: 0.4rem 0 0;
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.2rem 1rem;
}


.front-year-row {
	margin-top: 0.2rem;
}

/* --- Kassa ---------------------------------------------------------------- */

.cart-checkout {
	display: inline-flex;
	align-items: center;
	/* Centred along the row too. On a desktop the button is as wide as its
	   word and this changes nothing; on a phone it is stretched to the full
	   width of the cart, and without it KASSALLE sat against the left edge of
	   a button the width of the screen. */
	justify-content: center;
	border-radius: 999px;
	background: linear-gradient(140deg, var(--accent-mid), var(--accent-deep));
	color: #fff;
	font-weight: 700;
	letter-spacing: 0.04em;
	padding: 0.72rem 1.4rem;
	text-decoration: none;
}

/* The dealer's prose pages. One column for the whole page: the panel centres
   what it holds, so each part used to be centred on its own width and a
   section of two short sentences began further right than the one above it. */
.story {
	width: 100%;
	max-width: 46rem;
	/* Centred in the box, as the dealer wants it. The column itself is one
	   block, so every part still shares a left edge - that was the actual
	   complaint, and it is fixed by the wrapper rather than by the alignment. */
	justify-self: center;
	/* A grid item refuses by default to be narrower than its widest unbreakable
	   thing, and the price tables set their cells nowrap: on a phone the whole
	   column grew to 460 px inside a 330 px panel, and every paragraph on the
	   page ran out over the edge of the box with it. With this the column keeps
	   the panel's width and the table scrolls inside its own wrapper, which is
	   what the wrapper's overflow-x was for. */
	min-width: 0;
}

/* The lead is the page's first voice, so it is set larger than the body and
   stops before it becomes a second heading. */
.story-lead .front-detail-text {
	font-size: 1.12rem;
	line-height: 1.65;
}

.story-part {
	margin-top: 2rem;
}

/* The gap belongs BETWEEN parts, not above the first one. Yhteystiedot has no
   lead paragraph, so its first block was a part - and its text began 42 px
   below the heading where every other page began 10 px below it. The part's
   own heading carries a margin too (.front-terms-title, shared with the terms
   pages), so both have to go or the page still starts lower than its
   neighbours. */
.story > .story-part:first-child {
	margin-top: 0;
}

.story > .story-part:first-child > .front-terms-title {
	margin-top: 0;
}

/* A gap between paragraphs inside a part. .front-detail-text carries no
   margin of its own (it is used inside cards, where the card spaces things),
   so two paragraphs ran together into one block of text. */
.story .front-detail-text + .front-detail-text {
	margin-top: 0.7rem;
}

/* The question box's opener is a button, because it does something on this
   page rather than going somewhere. The global button rule makes an orange
   pill of it, so the pill is undone here - it belongs beside the two links
   and should look like them. */
.story-link-chat {
	appearance: none;
	background: none;
	border: 0;
	border-radius: 0;
	padding: 0;
	font: inherit;
	cursor: pointer;
}

/* AFTER the reset above, and that order is the rule: a primary button carries
   both classes (story-link-primary story-link-chat), they weigh the same, and
   written first it was undone by the reset - the page's one action rendered as
   plain text.

   The one thing the page asks the visitor to do, as a button that looks like
   one: the same pill as KASSALLE, because it is the same kind of step. One
   per page - a second orange button is two first choices. */
.story-link-primary {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	border-radius: 999px;
	background: linear-gradient(140deg, var(--accent-mid), var(--accent-deep));
	color: #fff;
	font-weight: 700;
	letter-spacing: 0.04em;
	padding: 0.72rem 1.4rem;
	border: 0;
	font-family: inherit;
	font-size: inherit;
	cursor: pointer;
}



.story-links {
	margin: 0.9rem 0 0;
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.5rem 0.7rem;
}

/* Said quietly: the fact that a link opens elsewhere is a warning, not part
   of its name. */
.story-link-note {
	font-size: 0.82rem;
	opacity: 0.7;
}

.story-link-sep {
	opacity: 0.4;
}

/* The table scrolls inside its own box rather than widening the page: four
   columns of prices do not fit a phone, and a page that pans sideways loses
   the text as well as the table. */
.story-table-wrap {
	margin: 0.9rem 0 1.1rem;
	overflow-x: auto;
}

/* The full width of the text it sits in.
   Not a min-width in rem, which is what it had: 26rem was chosen for the
   rental prices and ended the opening hours' rules half a screen past the
   last word and nowhere near the edge - a length that looked like neither.
   Full width is a decision the eye can see being kept. The wrapper still
   scrolls, so a table too wide for a phone is unchanged. */
.story-table {
	border-collapse: collapse;
	width: 100%;
}

.story-table th,
.story-table td {
	padding: 0.45rem 1.1rem 0.45rem 0;
	text-align: left;
	border-bottom: 1px solid rgba(255, 255, 255, 0.12);
	white-space: nowrap;
}

/* A short table is its words wide, in the middle of the page.
   Two columns of two words each, stretched from edge to edge, is a row with a
   gap in the middle of it - the day at one side of the box and the time at the
   other, with nothing in between to carry the eye across. The opening hours
   are the one table like that (see store-config.js). */
.story-table-compact {
	width: auto;
	margin-inline: auto;
}

/* The figures sit at the right edge, under each other.
   A table that runs the full width has to put its values somewhere, and left
   against a column boundary they float in the middle of the row with the rule
   carrying on past them. At the edge they line up with the edge, and the
   padding after the last one goes: there is nothing left to separate it from. */
.story-table td {
	text-align: right;
}

.story-table th:last-child,
.story-table td:last-child {
	padding-right: 0;
}

/* The prices line up on their digits; the row labels stay plain text weight,
   because the heading row is what says what the columns are. */
.story-table td {
	font-variant-numeric: tabular-nums;
}

/* The box's own text colour faded, NOT --muted or --ink: those tokens are a
   dark brown and a near-black for light surfaces, and this box is dark
   (rgba(10, 10, 10, 0.55)). Set from them, the heading row and the row
   labels were invisible - the table was there and its words were not. */
.story-table thead th:not(:first-child) {
	text-align: right;
}

.story-table thead th {
	color: rgba(244, 239, 230, 0.7);
	font-weight: 500;
	font-size: 0.85rem;
	letter-spacing: 0.06em;
	text-transform: uppercase;
}

.story-table tbody th {
	font-weight: 600;
	color: inherit;
}

/* One line set apart, once per page. Space does the setting apart, not a rule:
   an orange bar down the side of one sentence read as a warning or a quote
   from somewhere, and this is neither - it is the page's own line. */
.story-quote {
	margin: 2.6rem 0;
	max-width: 40rem;
	font-size: 1.22rem;
	line-height: 1.5;
}

/* The pickup points at the checkout: one row per point, the nearest first.
   A radio rather than a dropdown - there are five to eight of them and the
   address and the opening hours are what the choice is actually made on,
   which a dropdown cannot show. */
.checkout-pickup-list {
	display: grid;
	gap: 0.5rem;
	margin-top: 0.7rem;
}

/* White, like the fields above them and like Klarna's own rows: these are
   part of the form, and a dark card between white inputs read as something
   else on the page. The border and the radius are the input rule's, so the
   block lines up with the address fields it follows. */
.checkout-pickup-option {
	display: grid;
	grid-template-columns: auto 1fr;
	align-items: baseline;
	/* Tight beside the radio: the name is the widest thing here ("Postin
	   automaatti, S-market Rajamäki"), and every millimetre of indent was a
	   millimetre off the line it has to fit on. */
	gap: 0.05rem 0.4rem;
	padding: 0.55rem 0.7rem;
	background: rgba(255, 255, 255, 0.92);
	color: var(--ink);
	border: 1px solid rgba(24, 21, 17, 0.18);
	border-radius: 12px;
	cursor: pointer;
}

.checkout-pickup-option:hover {
	border-color: rgba(24, 21, 17, 0.4);
}

/* The rest of the points. A plain word rather than a button-shaped button:
   it opens a list, it does not place an order. */
.checkout-pickup-more {
	justify-self: start;
	margin-top: 0.1rem;
	padding: 0.35rem 0;
	background: none;
	border: 0;
	color: inherit;
	font: inherit;
	font-weight: 600;
	text-decoration: underline;
	text-underline-offset: 0.25em;
	cursor: pointer;
}

/* The radio keeps its native look - the global input rule would stretch it
   into a column-wide white box - and sits on the first row beside the name,
   not spanning the rows below it. Spanning was the bug: with the radio
   claiming every row of column 1, the browser placed the address in column 1
   of the next row and the rows came apart. */
.checkout-pickup-option input[type="radio"] {
	width: auto;
	grid-column: 1;
	grid-row: 1;
	margin: 0;
	accent-color: var(--accent);
}

/* Everything else in the second column, under the name. */
.checkout-pickup-option > span {
	grid-column: 2;
	min-width: 0;
}

.checkout-pickup-address,
.checkout-pickup-hours {
	font-size: 0.85rem;
	opacity: 0.7;
}

/* The name gets the whole second column, which is what makes "Postin
   automaatti, S-market Rajamäki" fit on one line. It may still wrap on a
   narrow phone - a cut-off name would hide which shop it is, and that is the
   one thing the row is for. */
.checkout-pickup-name {
	font-weight: 600;
}

/* Said before the form, not after it: a demo notice that appears once the
   order is placed has already failed at its job. */
.checkout-demo {
	margin: 0.6rem 0 1.8rem;
	padding: 1.2rem 1.1rem;
	border-left: 2px solid var(--accent);
	background: rgba(242, 106, 27, 0.12);
	font-size: 0.9rem;
	line-height: 1.5;
}

/* Views that fill the box.
 *
 * .front-panel centres its children (place-items: start center), and a centred
 * grid child is sized to its own content rather than stretched to the panel.
 * That is right for a heading or the front menu; for a whole view it means the
 * layout tracks its longest line. The checkout was visibly narrower than the
 * rest of the shop because of it, and changed width again once a logged-in
 * customer's contact details arrived prefilled - same box, two widths.
 *
 * The same opt-in .front-breadcrumb and .front-products already make, for the
 * same reason. */
#checkoutView,
#cartView,
.checkout-thanks {
	justify-self: stretch;
	width: 100%;
	/* Space under the final action.
	 *
	 * On the view rather than on .glass-box: the box also holds the front page
	 * and the category listings, which end in content rather than in a
	 * decision, and there the same gap would just be emptiness. */
	padding-bottom: var(--action-space);
}

/* The box stays the width of the shop; its contents do not.
 *
 * A cart row is an image, a name, a quantity and two amounts. Stretched across
 * 1120 px the name ends up far from the price it belongs to, and the eye has
 * to travel the whole box to pair them. Capped, they read as one row.
 *
 * Centred with auto margins rather than justify-self: the element still has to
 * stretch to the panel for the max-width to have a box to be measured against,
 * so the centring has to happen inside that stretched width. */
#cartView {
	max-width: 48rem;
	margin-inline: auto;
}

/* The thank-you page belongs to the same flow and is capped the same way, so
 * it is centred too - left in place it would read as a different page. */
.checkout-thanks {
	margin-inline: auto;
}

/* The heading and the demo note sit above the grid and would otherwise keep
 * the full box width, so the block would read as centred content under a
 * full-width header. 55.4rem is the grid's own width: 34 + 20 + the 1.4rem
 * gap. */
#checkoutView .front-title,
.checkout-demo {
	max-width: 55.4rem;
	margin-inline: auto;
}

/* The order confirmation: centred and clear of the box's bottom.
 *
 * The button used to be the form's last block element, i.e. at the left edge and
 * flush against the bottom of the box. It is the page's final act, so it deserves
 * room of its own - and a button with nothing below it looks as though the page
 * had been cut off.
 *
 * min-width rather than a width: the text changes mid-action ("ODOTETAAN
 * MAKSUA…"), and without a minimum width the button would jump at every step. */
#checkoutSubmit {
	display: block;
	margin: 1.6rem auto 0;
	min-width: 16rem;
	padding: 0.85rem 1.6rem;
	letter-spacing: 0.05em;
}

/* The return form.
 *
 * It uses the checkout's field and block styles: it is the same thing in a
 * different order, and a style family of its own would make two forms look
 * different for no reason. Only the line list is its own. */
#returnView {
	justify-self: stretch;
	width: 100%;
	max-width: 44rem;
	margin-inline: auto;
	padding-bottom: var(--action-space);
}

.return-form {
	margin-top: 1.1rem;
}

.return-order {
	margin: 1.2rem 0 0;
	padding: 0.9rem 1.1rem;
	border-left: 3px solid var(--accent);
	background: rgba(242, 106, 27, 0.1);
	border-radius: 0 10px 10px 0;
}

.return-order-head {
	margin: 0 0 0.3rem;
	font-weight: 700;
}

.return-window-ok,
.return-window-late {
	margin: 0;
	font-size: 0.9rem;
	line-height: 1.45;
}

/* A late notice is a remark rather than an error: the deadline starts from a
   delivery the shop does not know about, so red would read as a rejection. */
.return-window-late {
	color: rgba(244, 239, 230, 0.85);
}

.return-lines {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	gap: 0.5rem;
}

.return-line {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.5rem 0.9rem;
	padding: 0.6rem 0.8rem;
	border: 1px solid rgba(255, 255, 255, 0.18);
	border-radius: 10px;
}

/* A line already notified stays visible but dimmed: the customer has to see that
   the notice arrived, otherwise they will send it again. */
.return-line-done {
	opacity: 0.5;
}

.return-line-pick {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr);
	grid-template-areas: "box title" "box meta";
	align-items: center;
	gap: 0 0.7rem;
	flex: 1 1 16rem;
	cursor: pointer;
}

.return-line-pick input {
	grid-area: box;
	appearance: auto;
	width: auto;
	height: auto;
}

.return-line-title {
	grid-area: title;
	font-weight: 600;
}

.return-line-meta {
	grid-area: meta;
	font-size: 0.82rem;
	opacity: 0.7;
}

.return-line-qty {
	display: flex;
	align-items: center;
	gap: 0.4rem;
	font-size: 0.82rem;
}

.return-line-qty input {
	width: 4rem;
	height: 2.2rem;
	padding: 0 0.5rem;
	border: 1px solid rgba(255, 255, 255, 0.3);
	border-radius: 8px;
	background: rgba(255, 255, 255, 0.95);
	color: #14100d;
	font: inherit;
}

/* Everything already notified, said once at the top of the list. Disabled
   checkboxes say the same thing, but only to somebody who thinks to try them. */
.return-all-done {
	margin: 0 0 0.7rem;
	padding: 0.55rem 0.7rem;
	border-radius: 10px;
	border: 1px solid rgba(242, 106, 27, 0.35);
	background: rgba(242, 106, 27, 0.12);
	font-size: 0.86rem;
}

/* What the shop recorded, on the confirmation. The quantity is the part worth
   reading, so it carries the weight. */
.return-receipt {
	margin: 0.6rem 0 0.9rem;
	padding: 0;
	list-style: none;
	display: grid;
	gap: 0.3rem;
	font-size: 0.9rem;
}

.return-receipt-code {
	opacity: 0.55;
	font-variant-numeric: tabular-nums;
}

.return-line-flag {
	font-size: 0.78rem;
	font-style: italic;
}

.return-message textarea {
	width: 100%;
	box-sizing: border-box;
	padding: 0.6rem 0.7rem;
	border: 1px solid rgba(255, 255, 255, 0.3);
	border-radius: 10px;
	background: rgba(255, 255, 255, 0.95);
	color: #14100d;
	font: inherit;
	resize: none;
}

#returnSubmit,
#returnLookupBtn {
	display: block;
	margin: 1.4rem auto 0;
	min-width: 14rem;
	padding: 0.8rem 1.5rem;
	letter-spacing: 0.05em;
}

.checkout-grid {
	/* Ilmaa otsikon ja ensimmäisen lohkon väliin.
	 *
	 * TILAAJA-lohkon oma otsikko istuu laatikon yläreunassa, ja 32 pikselin
	 * päässä KASSA-otsikosta ne luettiin yhdeksi ryppääksi. Väli on gridillä
	 * eikä otsikolla, koska molemmat sarakkeet alkavat samalta korkeudelta ja
	 * niiden on liikuttava yhdessä - väli otsikon alla siirtäisi vain
	 * vasemman. */
	margin-top: 3.6rem;
	display: grid;
	/* The form column is sized, not "everything left over".
	 *
	 * .checkout-fields puts two inputs per row, so 1fr of a 1120 px box gave
	 * each field roughly 380 px - far wider than a postal code or a phone
	 * number ever needs, and wide enough that the label and the value it
	 * belongs to drift apart. 34rem keeps two fields at a comfortable size.
	 *
	 * justify-content: center so the sized pair sits in the middle of the box
	 * rather than against its left edge. */
	grid-template-columns: minmax(0, 34rem) minmax(0, 20rem);
	justify-content: center;
	gap: 1.4rem;
	align-items: start;
}

/* TILAAJA-lohko nousee puolen otsikkonsa verran.
 *
 * Fieldsetin reunaviiva kulkee legendin puolivälistä, joten laatikon piirretty
 * yläreuna jää 8 pikseliä alemmas kuin se kohta johon selain sen asettaa -
 * ja vieressä oleva TILAUS-laatikko, jolla ei ole legendiä, alkaa suoraan
 * ylhäältä. Sarakkeet olivat siis mitaten samalla korkeudella mutta näyttivät
 * eri korkuisilta.
 *
 * Nosto on tasan puolet legendin rivikorkeudesta (0,75rem × 1,33 ≈ 1rem), eli
 * se kumoaa juuri sen mitä selain tekee eikä ole silmämääräinen siirto. */
.checkout-grid > .checkout-form {
	margin-top: -0.5rem;
}

.checkout-block {
	border: 1px solid rgba(255, 255, 255, 0.16);
	border-radius: 12px;
	padding: 1rem 1.1rem 1.2rem;
	margin: 0 0 1.1rem;
}

.checkout-block legend {
	padding: 0 0.4rem;
	font-size: 0.75rem;
	font-weight: 700;
	letter-spacing: 0.16em;
	text-transform: uppercase;
	color: var(--accent);
}

.checkout-buyer-type {
	display: flex;
	gap: 1.4rem;
	margin-bottom: 1rem;
	font-size: 0.92rem;
}

.checkout-buyer-type label {
	display: flex;
	align-items: center;
	gap: 0.5rem;
	cursor: pointer;
	white-space: nowrap;
}

/* Radio buttons back into radio buttons.
   The global `input` rule gives every field width: 100%, a white background, a
   border and 0.7rem of padding. That is right for a text field, but for a radio it
   made a full-width white box with the label drawn on top of it - which is why
   "Yksityishenkilö" and "Yritys" looked as though they overlapped. The same fix is
   already in the .admin-checkbox-row rule. */
.checkout-buyer-type input[type="radio"],
.checkout-method input[type="radio"] {
	appearance: auto;
	-webkit-appearance: radio;
	/* auto rather than 1rem: a native radio draws itself at its own size, and a given
	   measure together with the border-radius clipped it - the circle was cut. The
	   global input rule forces width: 100%, so it has to be overridden, but auto hands
	   the decision back to the browser. */
	width: auto;
	height: auto;
	flex: none;
	margin: 0;
	padding: 0;
	border: 0;
	border-radius: 0;
	background: none;
	accent-color: var(--accent);
	cursor: pointer;
}

.checkout-fields {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 0.8rem 1rem;
}

@media (max-width: 620px) {
	.checkout-fields {
		grid-template-columns: minmax(0, 1fr);
	}

	/* A make too long for the line may use two of them. "KB Keith Black
	   Performance Motorcycle Pistons" is 395 px of unbreakable text in a 330 px
	   panel, and a phone has no room to spare - two lines of a make read as one
	   make, an unreadable page does not. */
	.front-model {
		white-space: normal;
	}

	/* The words may break onto two lines rather than push the table off the
	   screen: "SOFTAIL / DYNA" above the prices, "Viikonloppu (pe-su)" and
	   "Alennus, jos pyörään tehdään huoltoa" beside them. The figures keep
	   their nowrap - "1 099 €" broken after the thousand is a different
	   number. */
	.story-table th {
		white-space: normal;
	}

	/* And the padding between columns comes down with the room: 1.1rem three
	   times over is a third of a phone's width spent on gaps. */
	.story-table th,
	.story-table td {
		padding-right: 0.6rem;
	}
}

.checkout-field-wide {
	grid-column: 1 / -1;
}

.checkout-field span {
	display: block;
	font-size: 0.8rem;
	opacity: 0.75;
	margin-bottom: 0.25rem;
}

.checkout-field input {
	width: 100%;
	box-sizing: border-box;
	height: 2.5rem;
	padding: 0 0.7rem;
	border: 1px solid rgba(255, 255, 255, 0.3);
	border-radius: 10px;
	background: rgba(255, 255, 255, 0.95);
	color: var(--ink);
	font: inherit;
	font-size: 0.95rem;
}

.checkout-methods {
	display: flex;
	flex-direction: column;
	gap: 0.6rem;
}

/* The whole card is the target, not just the radio. */
.checkout-method {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr);
	/* A third row for the unavailability note. Empty when there is none: the
	   row gap is 0, so an unused row costs nothing. */
	grid-template-areas: "radio name" "radio text" "radio note";
	gap: 0 0.7rem;
	align-items: center;
	padding: 0.7rem 0.9rem;
	/* White like the fields and the pickup points: everything the buyer fills
	   in or chooses at the checkout is on paper, and the box around it is
	   not. A dark row among white ones read as a different kind of thing. */
	background: rgba(255, 255, 255, 0.92);
	color: var(--ink);
	border: 1px solid rgba(24, 21, 17, 0.18);
	border-radius: 10px;
	cursor: pointer;
}

.checkout-method:hover {
	border-color: rgba(24, 21, 17, 0.4);
}

/* The chosen one keeps the accent as a tint OF the white, not instead of it.
   A translucent orange replaces the background rather than layering on it,
   so the box's black showed through and the chosen row came out dark with
   dark text on it - the one row that has to be readable. */
.checkout-method:has(input:checked) {
	border-color: var(--accent);
	background: #fdeadb;
}

/* A method Klarna does not offer for this account.
 *
 * Kept visible rather than removed: the demo is shown to someone deciding
 * whether to open the card option in their contract, and a greyed-out row
 * says "one setting away" better than an absence can.
 *
 * Dimmed AND given a reason - dimming alone would read as a fault in the
 * shop. cursor: default because the row is a label and would otherwise still
 * offer a pointer for something that cannot be clicked. */
.checkout-method-off {
	opacity: 0.5;
	cursor: default;
}

.checkout-method-off:hover {
	border-color: rgba(24, 21, 17, 0.18);
}

/* The reason a method is unavailable. Dark now, like the row it sits in: it
   was set in the box's own light ink, which on white was almost invisible -
   the same trap the receipt's table fell into. */
.checkout-method-note {
	grid-area: note;
	margin-top: 0.25rem;
	font-size: 0.78rem;
	font-style: italic;
	color: rgba(24, 21, 17, 0.75);
}

/* The delivery options carry a figure, so they get a column for it: the name
   and the words on the left, the fee against the right edge in the size a
   cart line's total is set in. The payment methods below have no figure and
   keep their two columns. */
.delivery-choice .checkout-method {
	grid-template-columns: auto minmax(0, 1fr) auto;
	grid-template-areas: "radio name price" "radio text price" "radio note price";
}

.delivery-price {
	grid-area: price;
	align-self: center;
	padding-left: 0.8rem;
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.15rem;
	letter-spacing: 0.03em;
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/* The shop's own address under the collection option. Not italic like the
   unavailability note it shares a row with - this is an address, and an
   address set in italics reads as an aside rather than as somewhere to drive. */
.delivery-address {
	font-style: normal;
	line-height: 1.45;
}

/* The cart's copy of the choice. The checkout's white cards are right there -
   everything the buyer fills in is on paper - and wrong here, where the rows
   above are the shop's own dark cards and a white block would read as a
   different kind of thing altogether.
   Narrower than the cart rows, and against the right edge: the sums under it
   are read down their right-hand column, and this is the choice that decides
   one of them. */
.delivery-choice-dark {
	/* Wide enough for the shop's address on one line beside the fee. */
	max-width: 31rem;
	margin-inline-start: auto;
	margin-inline-end: 0;
}

.cart-rows + .delivery-choice,
.cart-missing + .delivery-choice {
	margin-block: 1.4rem 0.4rem;
}

.delivery-choice-dark .checkout-method {
	padding: 0.6rem 0.8rem;
	background: rgba(255, 255, 255, 0.05);
	border: 1px solid rgba(255, 255, 255, 0.12);
	border-radius: 12px;
	color: inherit;
}

.delivery-choice-dark .checkout-method:hover {
	border-color: rgba(255, 255, 255, 0.3);
}

/* The chosen one in the shop's colour, as a tint rather than a fill: the card
   keeps its dark ground, so the text on it stays the text it was. */
.delivery-choice-dark .checkout-method:has(input:checked) {
	border-color: var(--accent);
	background: rgba(242, 106, 27, 0.14);
}

.delivery-choice-dark .checkout-method-note {
	color: rgba(255, 255, 255, 0.72);
}

/* Where to come, on the thank-you page. The same box the pickup point gets in
   the confirmation email, because it answers the same question. */
/* The way back from a payment that did not go through. Beside the sentence
   that says so, because a customer reading bad news needs the next step in the
   same breath. */
.checkout-retry {
	margin: 0.6rem 0 0;
	font-size: 0.95rem;
	line-height: 1.5;
}

.checkout-pickup-note {
	margin: 1rem 0 0;
	padding: 0.7rem 0.9rem;
	border: 1px solid rgba(255, 255, 255, 0.2);
	border-radius: 10px;
	font-size: 0.9rem;
	line-height: 1.5;
}

.checkout-method input {
	grid-area: radio;
	width: auto;
	accent-color: var(--accent);
}

.checkout-method-name {
	grid-area: name;
	font-weight: 700;
	font-size: 0.95rem;
}

.checkout-method-text {
	grid-area: text;
	font-size: 0.82rem;
	opacity: 0.7;
}

.checkout-error {
	margin: 0 0 0.9rem;
	color: #ff8a7a;
	font-weight: 700;
	font-size: 0.9rem;
}

/* The order summary emphasised: it is the checkout's most important block,
   because it says what the customer is about to pay. The accent colour in the
   borders and as warmth in the background, much the way Klarna lifts the total
   out.
   The background is only at 0.10 opacity: a stronger tint starts eating into the
   text's contrast, and the total is the thing that has to be readable. */
.checkout-summary {
	border: 1px solid var(--accent);
	border-radius: 12px;
	padding: 1.1rem 1.2rem;
	background: rgba(242, 106, 27, 0.1);
	box-shadow: 0 0 0 3px rgba(242, 106, 27, 0.12);
}

.checkout-summary .checkout-lines li {
	border-bottom-color: rgba(242, 106, 27, 0.35);
}

/* The grand total is set apart with the accent colour rather than a grey rule, so
   that within the summary it is the row that registers first. */
.checkout-summary .checkout-total-row {
	border-top-color: var(--accent);
	color: #fff;
}

.checkout-lines {
	list-style: none;
	margin: 0.7rem 0 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 0.7rem;
}

.checkout-lines li {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	grid-template-areas: "title sum" "meta sum";
	gap: 0 0.6rem;
	padding-bottom: 0.7rem;
	border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.checkout-line-title {
	grid-area: title;
	font-size: 0.88rem;
	line-height: 1.3;
	color: inherit;
	text-decoration: none;
}

.checkout-line-title:hover,
.checkout-line-title:focus-visible {
	color: var(--accent-soft);
}

.checkout-line-meta {
	grid-area: meta;
	font-size: 0.78rem;
	opacity: 0.65;
}

.checkout-line-sum {
	grid-area: sum;
	align-self: center;
	font-weight: 700;
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

.checkout-totals {
	margin: 1rem 0 0;
	display: flex;
	flex-direction: column;
	gap: 0.4rem;
	font-size: 0.9rem;
}

.checkout-totals div {
	display: flex;
	justify-content: space-between;
	gap: 1rem;
}

.checkout-totals dt,
.checkout-totals dd {
	margin: 0;
}

.checkout-totals dd {
	font-variant-numeric: tabular-nums;
}

.checkout-total-row {
	margin-top: 0.4rem;
	padding-top: 0.6rem;
	border-top: 1px solid rgba(255, 255, 255, 0.2);
	font-size: 1.05rem;
	font-weight: 800;
}

.checkout-thanks {
	max-width: 26rem;
}

.checkout-order-number {
	font-size: 1.6rem;
	font-weight: 800;
	letter-spacing: 0.04em;
	color: var(--accent);
	margin: 0 0 0.5rem;
}

/* The label in the body text's own size and colour, so the number stays the
 * thing the eye lands on. Making the whole line large and orange would give
 * the word "Tilausnumero:" the same weight as the number it introduces. */
.checkout-order-label {
	/* On the same line as the number, not above it. Inline so the newline in
	 * the markup renders as the single space between label and number. */
	font-size: 0.85rem;
	font-weight: 600;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	/* The box's own text colour faded, not --muted: that token is a dark brown
	 * for light surfaces, and this box is dark (rgba(10, 10, 10, 0.55)). It
	 * would have been very nearly invisible here. */
	color: rgba(244, 239, 230, 0.75);
}

/* Payment outcome on the thank-you page.
 *
 * The three states are told apart by a left bar and weight rather than colour
 * alone: the failure case is the one the customer has to act on, and colour on
 * its own does not reach a reader who cannot distinguish it. */
.checkout-payment-ok,
.checkout-payment-wait,
.checkout-payment-fail {
	margin: 0 0 0.75rem;
	padding: 0.6rem 0.9rem;
	border-left: 4px solid var(--accent);
	background: rgba(242, 106, 27, 0.08);
	font-size: 0.95rem;
	line-height: 1.45;
}

.checkout-payment-ok {
	border-left-color: #2e7d32;
	background: rgba(46, 125, 50, 0.08);
}

.checkout-payment-fail {
	font-weight: 600;
	border-left-color: #b3261e;
	background: rgba(179, 38, 30, 0.08);
}

/* Klarna's widget sits inside the payment block, so it gets the same inset as
 * the method list rather than sitting flush against the fieldset edge. */
/* Klarna draws its own widget in here, in its own colours: dark text meant for
   a white page. On this page it landed on the dark box and read as an empty
   line with a tick in it - the words were there and could not be seen.
   The container is therefore paper, like the payment cards and the fields
   above it: everything the buyer reads or fills in at the checkout is on
   white, and Klarna's part of it is no exception. Empty until Klarna has
   something to show, so the padding and the border come with the content
   rather than sitting there as a blank card. */
.checkout-klarna {
	margin-top: 0.9rem;
	min-height: 1px;
}

.checkout-klarna:not(:empty) {
	padding: 0.8rem 0.9rem;
	background: rgba(255, 255, 255, 0.92);
	color: var(--ink);
	border: 1px solid rgba(24, 21, 17, 0.18);
	border-radius: 10px;
}

/* --- Admin: tilaukset ----------------------------------------------------- */

/* An order that needs a human. Weight as well as colour, for the same reason
 * as the thank-you page. */
.admin-order-warn {
	color: #b3261e;
	font-weight: 600;
}

/* Order search. One field, full width.
 *
 * A narrow field would have hinted at a short search term, but a full address or
 * an email address is typed in here just as often. */
.admin-order-search {
	margin: 0.9rem 0 1.1rem;
}

.admin-order-search input {
	width: 100%;
	height: 2.6rem;
	padding: 0 0.85rem;
	border: 1px solid rgba(255, 255, 255, 0.25);
	border-radius: 10px;
	background: rgba(255, 255, 255, 0.95);
	color: #14100d;
	font: inherit;
}

.admin-order-count {
	margin: 0.4rem 0 0;
	font-size: 0.82rem;
	opacity: 0.75;
}

/* Empty it takes no room: without a search the row would be an empty paragraph
   pushing the list down for no reason. */
.admin-order-count:empty {
	display: none;
}

/* The number of unhandled returns in the PALAUTUKSET button.
 *
 * In the button rather than as a separate badge: a return is something that has to
 * be acted on, and the count is the only thing that says whether the section needs
 * visiting. Zero is not shown at all - :empty hides the whole badge, because a
 * number demanding attention for nothing is noise. */
/* Bigger, and centred on the word rather than sitting on its baseline: the
   badge is read at a glance from across the menu, and a small disc hanging
   under the letters reads as a footnote. inline-flex with a fixed height
   makes it a circle whatever the digit, and vertical-align: middle puts its
   centre on the text's. */
.admin-nav-count {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	vertical-align: middle;
	margin-left: 0.45rem;
	min-width: 1.8rem;
	height: 1.8rem;
	padding: 0 0.42rem;
	border-radius: 999px;
	background: #b3261e;
	color: #fff;
	font-size: 0.92rem;
	line-height: 1;
	text-align: center;
	/* Two pixels up. The menu's own letters are capitals, so their optical
	   middle sits above the line a disc centres itself on - matched to the
	   baseline the badge looked as if it were sinking. */
	position: relative;
	top: -2px;
}

.admin-nav-count:empty {
	display: none;
}

/* Something is waiting. The same orange and the same pulse as the inbox
   bubble, because it means the same thing in a different place - one signal
   to learn instead of a symbol per section. The red pill stays for counts
   that are merely information (the words that found nothing). */
.admin-nav-count.admin-nav-uusi {
	background: var(--accent);
	color: #14100d;
	animation: admin-nav-syke 2s ease-out infinite;
}

@keyframes admin-nav-syke {
	0%   { box-shadow: 0 0 0 0 rgba(242, 106, 27, 0.55); }
	70%  { box-shadow: 0 0 0 0.6rem rgba(242, 106, 27, 0); }
	100% { box-shadow: 0 0 0 0 rgba(242, 106, 27, 0); }
}

/* A pulse is an invitation to look, not a requirement: for a reader who has
   asked for less motion the colour alone carries it. */
@media (prefers-reduced-motion: reduce) {
	.admin-nav-count.admin-nav-uusi {
		animation: none;
	}
}

/* A handled notice stays in the list, dimmed. Removing it would turn the list into
   a work list with no history, and a return is an event that belongs in the
   books. */
.admin-return-done {
	opacity: 0.6;
}

.admin-return-flag {
	margin: 0.6rem 0 0;
	font-size: 0.82rem;
	font-style: italic;
	opacity: 0.8;
}

/* A return card: the number and the refund on the top row, the state under
   the number, and the one action on a row of its own at the bottom right.
   .admin-order-sum carries no area of its own - on an order it sits inside a
   wrapper - so the returns card places it here. */
.admin-return-item > .admin-order-sum {
	grid-area: sum;
	justify-self: end;
}

.admin-return-item > .admin-return-flag {
	grid-area: date;
	margin: 0.15rem 0 0;
}

/* A handled order, dimmed like a handled return: it stays in the list,
   because the list is the shop's history and not a work queue that empties.
   The state line under the number says when. */
.admin-order-done {
	opacity: 0.55;
}

/* A handled order's money in pale orange. The card is history once it is
   marked, but the sum is the one figure still read off it - down the list of
   what the shop has sold - and orange is where the eye goes on these cards. */
.admin-order-done .admin-order-sum {
	color: var(--accent-soft);
}

/* The order's decision on its own last row, right-aligned and the same size
   as the one on a return - the two lists ask the same question and should
   ask it in the same voice. The margin is the distance from the print icons:
   they are done while packing, this is done when the parcel is gone. */
/* "Tilaus on matkalla" at the foot left, opposite the handled mark: telling
   the customer and closing the order are two different moments, and putting
   them at opposite ends says so without a word. An envelope, because what
   the press does is send a message. */
.admin-order-shipped {
	grid-area: action;
	justify-self: start;
	/* align-self, or the button stretches to the row's height - grid items
	   default to stretch, and the row is as tall as the tallest thing in it.
	   That is what made this one ten pixels taller than the button opposite,
	   which sets its own height by being content-sized. */
	align-self: center;
	display: inline-flex;
	align-items: center;
	gap: 0.45rem;
	margin: 2.4rem 0 0.15rem;
	/* The same padding as MERKITSE KÄSITELLYKSI opposite it: the two are one
	   pair of controls and a different height on one of them reads as a
	   different kind of control. Wider is fine - it carries more words. */
	padding: 0.5rem 1rem;
	background: none;
	border: 1px solid rgba(255, 255, 255, 0.22);
	border-radius: 999px;
	color: #fff;
	font: inherit;
	font-size: 0.82rem;
	font-weight: 700;
	line-height: 1.15;
	letter-spacing: 0.04em;
	cursor: pointer;
	opacity: 0.85;
}

.admin-order-shipped:hover:not(:disabled) {
	opacity: 1;
	border-color: var(--accent);
	color: var(--accent);
}

/* A handled order has nothing left to announce, so the button goes quiet
   rather than away: the dealer can see what it would have done, and the key
   beside it brings it back. */
.admin-order-shipped:disabled {
	opacity: 0.35;
	cursor: default;
}

/* The envelope is sized in em, so it follows the label rather than driving
   the button's height: at 1.05rem it made this button ten pixels taller than
   the one opposite it, and two controls of a pair should be one height. */
.admin-order-shipped svg {
	width: 1em;
	height: 1em;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.6;
	stroke-linecap: round;
	stroke-linejoin: round;
}

/* What was told and when, under the button that told it. The code is a link
   into Posti's tracking with the code already filled in - the dealer checks
   the same page the customer does. */
.admin-order-shipped-state {
	grid-area: state;
	margin: 0.15rem 0 0;
	font-size: 0.78rem;
	opacity: 0.6;
}

.admin-tracking-link {
	color: var(--accent);
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

/* Both cards: the decision on its own row at the foot, right-aligned, with
   room on either side - above so it is not read as the last line of the
   details, below so it does not sit on the rule that ends the card.
   
   grid-area and justify-self belong HERE rather than in a rule of their own:
   without them the button has no place in the grid, and a grid item with no
   place stretches across the row - which is exactly how it came out when
   this rule was written as margins alone. */
/* The same margins as the mark opposite, written with the same weight of
   selector. .admin-panel button sets margin-top: 1rem and a single class does
   not beat it, so this button was starting 4.8 px higher than the one placed
   by the two-class rule below - which is exactly the gap that showed. */
.admin-order-item > .admin-order-shipped {
	margin: 2.4rem 0 0.15rem;
}

.admin-order-item > .admin-order-handled,
.admin-order-item > .admin-order-reopen,
.admin-return-item > .admin-return-handled,
.admin-return-item > .admin-return-reopen {
	grid-area: action;
	justify-self: end;
	/* Centred like the button opposite, so the pair sits on one line. The
	   two are different heights - a key is round, a word is not - and only a
	   shared centre keeps them level whichever is the taller. */
	align-self: center;
	margin: 2.4rem 0 0.15rem;
}

/* The key that opens a handled notice again. An icon rather than a button
   with a word: reopening is the rare case, and a full-width control invites
   what it should merely allow. */
.admin-return-reopen,
.admin-order-reopen {
	width: 2.2rem;
	height: 2.2rem;
	padding: 0;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	background: none;
	/* Orange rather than white, and at full strength: the card it sits on is
	   dimmed to 55 %, so white at three quarters came out a grey smudge on a
	   grey card. The dimming turns this into the pale orange the sums on a
	   handled card already have, which is the colour that reads there. */
	border: 1px solid var(--accent-soft);
	border-radius: 999px;
	color: var(--accent);
	opacity: 1;
	cursor: pointer;
}

.admin-return-reopen:hover,
.admin-order-reopen:hover {
	border-color: var(--accent);
	color: var(--accent);
	opacity: 1;
}

.admin-return-reopen svg,
.admin-order-reopen svg {
	width: 1.1rem;
	height: 1.1rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.8;
	stroke-linecap: round;
	stroke-linejoin: round;
}

/* An unhandled notice states its own arrival in the shop's colour: the line
   is a state, and this is the state that is waiting for somebody. A handled
   one keeps the plain grey - it is history by then. */
.admin-return-flag-new {
	margin-bottom: 0.5rem;
	color: var(--accent);
	opacity: 1;
}

/* One size for the same question in both lists. The order card used the
   default button, which is a third larger, and a decision that looks bigger
   on one card than on the other reads as a different decision. */
.admin-return-handled,
/* The back office's button size, from the one place it is written down.
   See --admin-button-padding in :root. */
.admin-order-handled,
.admin-panel .admin-catalogue-check {
	width: auto;
	padding: var(--admin-button-padding);
	/* A transparent border, because the button opposite has a real one: the
	   two pixels are the difference between the pair matching and nearly
	   matching. */
	border: 1px solid transparent;
	font-size: var(--admin-button-size);
	line-height: var(--admin-button-line);
}

/* The month tabs. A row that scrolls rather than wraps: wrapped, a shop with
   three years of orders would push the list off the screen, and the months
   nobody is looking at are the ones that should be out of sight. */
.admin-month-tabs-row {
	display: flex;
	align-items: center;
	gap: 0.4rem;
	margin: 0.9rem 0 0.2rem;
}

.admin-month-tabs {
	display: flex;
	gap: 0.4rem;
	overflow-x: auto;
	scrollbar-width: none;
	scroll-behavior: smooth;
}

.admin-month-tabs::-webkit-scrollbar {
	display: none;
}

.admin-month-tab {
	flex: none;
	background: none;
	border: 1px solid rgba(255, 255, 255, 0.18);
	border-radius: 999px;
	padding: 0.32rem 0.75rem;
	color: #fff;
	font: inherit;
	font-size: 0.78rem;
	font-weight: 700;
	letter-spacing: 0.04em;
	white-space: nowrap;
	opacity: 0.7;
	cursor: pointer;
}

.admin-month-tab:hover {
	opacity: 1;
	border-color: rgba(255, 255, 255, 0.4);
}

/* The month being read, in the shop's own colour - the same mark the menu
   uses for the section being read. */
.admin-month-tab-on {
	background: var(--accent);
	border-color: var(--accent);
	color: #14100d;
	opacity: 1;
}

/* How much of that month is still waiting, right after its name. In the
   shop's colour, because it is the same "somebody has to do this" the nav
   badges carry - and written only when it is more than none. */
.admin-month-open {
	margin-left: 0.2rem;
	color: var(--accent);
	font-variant-numeric: tabular-nums;
}

/* On the chosen tab the ground is already orange, so the number takes the
   dark it sits on and keeps its weight by being solid rather than coloured. */
.admin-month-tab-on .admin-month-open {
	color: #14100d;
	opacity: 0.7;
}

/* Which half of the list, under the months and against the right edge: two
   checkboxes, one ticked and one empty, and the one being read is lit. The
   pair says what a single tick cannot - that both halves are there, and which
   of them this is. */
.admin-state-row {
	display: flex;
	justify-content: flex-end;
	gap: 0.2rem;
	margin: 0.35rem 0 0;
}

.admin-state-tab {
	display: inline-flex;
	align-items: center;
	gap: 0.3rem;
	padding: 0.25rem 0.45rem 0.25rem 0.3rem;
	background: none;
	border: 0;
	border-radius: 8px;
	color: #fff;
	opacity: 0.35;
	cursor: pointer;
	line-height: 0;
}

.admin-state-tab svg {
	width: 1.25rem;
	height: 1.25rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.7;
	stroke-linecap: round;
	stroke-linejoin: round;
}

/* How many are in that half of the month. Always written, zero included: a
   box with no number beside it says nothing, and "0" says the half is empty -
   which is what the dealer came to find out. */
.admin-state-count {
	font-size: 0.78rem;
	font-weight: 700;
	line-height: 1;
	font-variant-numeric: tabular-nums;
}

.admin-state-tab:hover {
	opacity: 0.7;
}

/* The ticked box is pale orange whichever half is being read - it is the mark
   a handled thing carries everywhere in the panel, the same pale orange as a
   handled order's sum. The empty box is plain: nothing has happened to it. */
.admin-state-tab[data-tila="handled"] {
	color: var(--accent-soft);
}

/* Which one is being read: lit, and standing on a pill of its own. Colour
   alone could not say it, since the two icons are different colours to begin
   with. */
.admin-state-tab-on,
.admin-state-tab-on:hover {
	background: rgba(255, 255, 255, 0.1);
	opacity: 1;
}

/* The arrow, shown only when there is something past the edge. */
.admin-month-more {
	flex: none;
	width: 1.9rem;
	height: 1.9rem;
	padding: 0;
	background: none;
	border: 1px solid rgba(255, 255, 255, 0.18);
	border-radius: 999px;
	color: #fff;
	font: inherit;
	font-size: 0.85rem;
	line-height: 1;
	cursor: pointer;
	opacity: 0.7;
}

.admin-month-more:hover {
	opacity: 1;
	border-color: var(--accent);
	color: var(--accent);
}

.admin-order-list {
	list-style: none;
	margin: 0.8rem 0 0;
	padding: 0;
	display: flex;
	flex-direction: column;
}

.admin-order-item {
	padding: 0.9rem 0;
	border-top: 1px solid rgba(255, 255, 255, 0.12);
}

/* The two print buttons, above the money in the same right-hand column. */
/* The print buttons on a row of their own, under the order and to the right.
   They are what you reach for once the order has been read, so they come after
   the reading rather than beside it - and on their own row nothing else has to
   make space for them. */
.admin-order-tools {
	grid-area: tools;
	justify-self: end;
	display: flex;
	/* The two belong together - one document for the parcel, one for inside it -
	   so they sit as a pair, and the air goes between them and the order rather
	   than between them. */
	gap: 0.15rem;
	padding-top: 1.4rem;
}

.admin-order-print {
	padding: 0.45rem;
	border: 0;
	border-radius: 8px;
	background: none;
	color: inherit;
	opacity: 0.45;
	cursor: pointer;
	line-height: 0;
}

/* Isommat 8.10.2026: nämä kolme ovat kortin ainoat napit sen jälkeen kun
   POSTIN LÄHETYS ja MALLILAPPU poistuivat, ja ne tehdään pakatessa - käsi
   kädessä laatikon kanssa, usein puhelimen ruudulta. 1,6 rem oli koko jolla ne
   olivat yksi rivi monen napin joukossa. */
.admin-order-print svg {
	width: 2.1rem;
	height: 2.1rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.7;
	stroke-linecap: round;
	stroke-linejoin: round;
}


/* The combined icon is two drawings and a plus, so its box is wider - 44 units
   against 24, and the width follows so the scale stays the same. The parcel
   and the receipt then come out exactly the size they are on their own
   buttons; what is new is the room between them. */
.admin-order-print-both svg {
	width: 3.85rem;
}

.admin-order-print:hover:not(:disabled),
.admin-order-print:focus-visible:not(:disabled) {
	background: none;
	opacity: 1;
	color: var(--accent);
}

/* The parcel's papers on an order nobody is posting. Dimmed rather than gone,
   like the notice button opposite: the dealer can see what would be printed
   for a posted order, and hovering says why it is not. */
.admin-order-print:disabled {
	opacity: 0.18;
	cursor: default;
}

/* The order's money, stacked: what was ordered, what came back, what is left.
   One column so the figures line up under each other and can be read down. */
.admin-order-sums {
	grid-area: sum;
	justify-self: end;
	/* On the order number's line. The area spans the rows under it - the time,
	   the tracking code, when it was sent - and centred in all of them the
	   figure drifted down among the small grey text. The right-hand edge is
	   unchanged: only the height moved. */
	align-self: start;
	display: grid;
	justify-items: end;
	gap: 0.1rem;
}

.admin-order-sum {
	margin: 0;
	font-weight: 800;
	font-size: 1.1rem;
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/* The original total of an order that has been partly returned. Struck
   through, because it is no longer the amount - and dimmed, because the eye
   should land on the one below it. */
.admin-order-sum-was {
	opacity: 0.45;
	font-weight: 600;
	text-decoration: line-through;
}

.admin-order-sum-back {
	color: var(--accent-soft);
}

/* What is left. The same size as the total was, because it is now the total
   that matters - a correction in small text under a large number reads as a
   footnote. */
.admin-order-sum-net {
	color: var(--accent);
}

/* --- Admin: tilaustilasto ------------------------------------------------- */

/* The list to the left, the figures to the right. The .admin-panel prefix because
   .admin-grid is defined later in the file at the same specificity. */
.admin-panel .admin-grid-orders {
	grid-template-columns: minmax(0, 1fr) minmax(0, 21rem);
	gap: 0.9rem;
	align-items: start;
	justify-content: start;
}

/* Two halves, for a view whose two boxes are equals: the search words and the
   page counts answer the same question from two sides, so neither gets the
   narrow column that suits a side note. */
.admin-panel .admin-grid-halves {
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 0.9rem;
	/* Stretch, not start: two boxes of the same width and different heights
	   read as one box and one leftover. */
	align-items: stretch;
}

/* The two analytics lists: a word or a page name on the left, its count on
   the right. .admin-order-head is a plain paragraph elsewhere, so the spans
   ran together - "Etusivu240 avausta" - and the numbers did not line up in a
   column where they could be compared. Scoped to these two lists by id
   rather than changed on the shared class, which the orders and returns
   views also use. */
#adminSearchList .admin-order-head,
#adminViewList .admin-order-head {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.4rem;
}

#adminSearchList .admin-search-hits,
#adminViewList .admin-search-hits {
	margin-left: auto;
	white-space: nowrap;
}

@media (max-width: 1100px) {
	.admin-panel .admin-grid-halves {
		grid-template-columns: minmax(0, 1fr);
	}

	.admin-panel .admin-grid-orders {
		grid-template-columns: minmax(0, 1fr);
	}
}

.admin-chart {
	margin: 0.8rem 0 0;
}

.admin-chart-svg {
	display: block;
	width: 100%;
	height: auto;
	color: #fff;
}

.admin-month-list {
	list-style: none;
	margin: 1.2rem 0 0;
	padding: 0;
	display: flex;
	flex-direction: column;
}

/* The month and the order count to the left, the totals to the right on rows of
   their own. tabular-nums, so the decimal commas line up across rows. */
.admin-month-list li {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	grid-template-areas: "name sum" "count vat";
	gap: 0 0.8rem;
	padding: 0.55rem 0;
	border-top: 1px solid rgba(255, 255, 255, 0.1);
	font-size: 0.85rem;
}

.admin-month-name {
	grid-area: name;
	font-weight: 700;
}

.admin-month-count {
	grid-area: count;
	font-size: 0.78rem;
	opacity: 0.65;
}

.admin-month-sum {
	grid-area: sum;
	text-align: right;
	font-weight: 700;
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

.admin-month-vat {
	grid-area: vat;
	text-align: right;
	font-size: 0.78rem;
	opacity: 0.65;
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/* The last few carts that stopped at the payment, under the month's figures.
   Four small columns: when, how much, what was in it, and which method said
   no. No name anywhere, because none is stored. */
/* Under the order list, set apart from it by a rule: the list above is orders
   and this is not, and the two must not read as one list with odd rows in it. */
.admin-attempts {
	margin-top: 1.2rem;
	padding-top: 0.2rem;
	border-top: 1px solid rgba(255, 255, 255, 0.12);
}

.admin-attempts-title {
	margin: 0.9rem 0 0.4rem;
}

.admin-attempt-list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	font-size: 0.78rem;
}

.admin-attempt-list li {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	gap: 0 0.8rem;
	padding: 0.4rem 0;
	border-top: 1px solid rgba(255, 255, 255, 0.08);
}

.admin-attempt-when {
	opacity: 0.7;
}

.admin-attempt-sum {
	text-align: right;
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/* What was in the basket, and what refused it: both on the second row, where
   the month rows put their small print. */
.admin-attempt-what {
	grid-column: 1 / -1;
	opacity: 0.85;
	word-break: break-word;
}

.admin-attempt-why {
	grid-column: 1 / -1;
	opacity: 0.55;
}

/* Carts that stopped at the payment. Grey rather than red: it is a fact about
   the month, not a fault to be fixed this afternoon - and the number nobody
   can act on individually, because there is no customer behind it. */
.admin-month-attempts {
	grid-column: 1 / -1;
	margin-top: 0.15rem;
	font-size: 0.75rem;
	opacity: 0.5;
}

.admin-month-total {
	margin-top: 1.2rem;
	padding-top: 0.9rem;
	border-top: 1px solid rgba(255, 255, 255, 0.2);
}

/* The totals read as the last row of the months above them, so they are set
   the same way: the name on the left, the figure against the right edge, on
   tabular figures so the decimal commas line up with the months' own. In the
   catalogue's own meta list the values share a left edge, which is right for
   text and wrong for money - a column of euros is read down its commas. */
.admin-month-total .admin-catalogue-row {
	grid-template-columns: minmax(0, 1fr) auto;
	gap: 0 0.8rem;
}

.admin-month-total .admin-catalogue-row dd {
	text-align: right;
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/* An order row: the number and the date on rows of their own, the total to the
   right. */
.admin-order-item {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	/* A row for the card's own decision, under everything else: on an order
	   that is MERKITSE KÄSITELLYKSI, and it is not one of the print icons. */
	grid-template-areas: "number sum" "date sum" "pay sum" "track sum" "state sum" "tools tools" "meta meta" "action action";
	gap: 0 1rem;
}

.admin-order-number {
	grid-area: number;
	margin: 0;
	font-size: 1.05rem;
	font-weight: 800;
	letter-spacing: 0.06em;
	color: var(--accent);
	font-variant-numeric: tabular-nums;
}

.admin-order-date {
	grid-area: date;
	margin: 0.1rem 0 0;
	font-size: 0.82rem;
	opacity: 0.7;
}

/* An order nobody paid for. The card keeps its place in the list - the
   customer may still come back - but it is not work, so it is set apart the
   way a handled order is, and every control that would move it towards the
   customer is locked (see tilausKortti). */
.admin-order-unpaid-item {
	opacity: 0.6;
}

/* The invoice's own line: what the shop has asked for and when it falls due,
   with the one control that belongs to the state it is in. On the same row as
   the other payment lines, because it IS the payment state for an order the
   shop collects itself. */
/* The invoice, on a white slip of its own.
   A line of its own under the date, and the one thing on the card with the
   colours of paper: it is what the customer is holding, and the dealer reads
   the number and the due date off it while somebody is on the phone. Faint
   grey on a dark card is the wrong weight for that.
   justify-self: start so the slip is as wide as its sentence - a band running
   the width of the card would read as a section rather than a line. */
.admin-order-invoice {
	grid-area: pay;
	justify-self: start;
	margin: 0.35rem 0 0.15rem;
	padding: 0.2rem 0.55rem;
	border-radius: 4px;
	background: #fff;
	color: #1a1a1a;
	/* Not flex: the row is a sentence with a link in it, and flex made it a
	   block on a line of its own. Inline keeps it in the sentence and lets the
	   whole thing wrap where a sentence wraps. */
	font-size: 0.82rem;
	line-height: 1.6;
}

/* A <button> is display: inline-block by default, but the global button rule
   gives these a block-ish shape - inline keeps them inside the sentence they
   belong to. The colour is set here because the shop's orange is chosen for a
   dark card and disappears on this one. */
.admin-order-invoice .admin-text-link {
	display: inline;
	margin-left: 0.5rem;
	padding-bottom: 0;
	color: #9a4a00;
}

/* The two payment lines that need saying, at the top where the state is read.
   Red for the one that must not be packed at all; the shop's orange for the
   one that must not be packed yet - a review can still become a payment, and
   red would read as a refusal. */
.admin-order-unpaid,
.admin-order-pending {
	grid-area: pay;
	margin: 0.15rem 0 0;
	font-size: 0.82rem;
	font-weight: 700;
}

.admin-order-unpaid {
	color: #ff6b57;
}

.admin-order-pending {
	color: var(--accent);
}

/* The third box: orders whose payment did not go through. Red rather than the
   pale orange a handled thing carries - this is not a state anything reached
   on purpose. */
.admin-state-tab-unpaid {
	color: #ff6b57;
}

/* The tracking code with the order's other numbers. Grey like the time above
   it, because it is a fact about the order and not a thing to press - only
   the code itself is a link. */
.admin-order-tracking {
	grid-area: track;
	margin: 0.15rem 0 0;
	font-size: 0.82rem;
	opacity: 0.7;
}

.admin-order-item > .admin-catalogue-meta {
	grid-area: meta;
}

/* --- Google-kirjautuminen ------------------------------------------------- */

/* Google's button renders its own iframe or div structure, so its insides are not
   ours to style - nor should they be. Google requires the use of its own button,
   and a styled imitation would both break the terms and deceive the user. This
   block only reserves a place for it. */
.topbar-google {
	display: flex;
	align-items: center;
	min-height: 32px;
}

/* --- Kirjautumisdialogi --------------------------------------------------- */

/* A native <dialog>. ::backdrop comes from the browser, so the dimming behind it
   does not have to be built by hand - and its z-index does not have to be
   guessed. */
.login-dialog {
	width: min(24rem, calc(100vw - 2rem));
	/* Equal air above and below. The close button is lifted out of the flow
	   (below), so the padding is the whole of it: the heading starts one
	   padding from the top and the last thing in the dialog ends one padding
	   from the bottom. With the × in the flow the top read as half again as
	   much space as the bottom - 46 px against 23. */
	position: relative;
	padding: 1.6rem;
	border: 1px solid rgba(255, 255, 255, 0.18);
	border-radius: 16px;
	background: #1a1a1a;
	color: #f4efe6;
	box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}

.login-dialog::backdrop {
	background: rgba(0, 0, 0, 0.6);
	backdrop-filter: blur(2px);
}

/* The × in the corner rather than on a row of its own: a row of its own is
   space the dialog then has to answer for at the bottom too. */
.login-dialog-close-form {
	position: absolute;
	top: 0.5rem;
	right: 0.5rem;
	display: flex;
	margin: 0;
}

/* The border-radius is set here rather than inherited. The global button rule
   gives 999px, and it showed: on opening, a <dialog> focuses the first focusable
   element, i.e. this button, and the browser's own focus ring followed the
   inherited rounding - so a blue circle nobody had asked for appeared around the
   X.
   The focus ring is not removed but replaced: somebody moving by keyboard has to
   see where they are. */
.login-dialog .login-dialog-close {
	background: none;
	border: 0;
	border-radius: 6px;
	padding: 0.2rem 0.45rem;
	font-size: 1.4rem;
	line-height: 1;
	color: rgba(244, 239, 230, 0.7);
	cursor: pointer;
}

.login-dialog .login-dialog-close:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
	color: #fff;
}

/* Orange on hover. Measured against the dialog's #1a1a1a background:

     resting  0.7 white   8.00:1
     hover    --accent    5.68:1

   The × is 1.4rem, i.e. large text, whose AA threshold is 3:1, so both are well
   above it. The hover is darker than the resting state but clearly coloured, and
   that colour change is what says the target has been hit. */
.login-dialog .login-dialog-close:hover {
	background: none;
	color: var(--accent);
}

/* The terms in a window over the checkout. Wider and taller than the login
   dialog, because this one is read rather than filled in - and its own
   scroller, so the page behind does not move while somebody is halfway down
   section 5. */
.legal-dialog {
	width: min(44rem, calc(100vw - 2rem));
	max-height: min(80vh, 48rem);
}

/* display ONLY while open.
 *
 * A closed <dialog> is hidden by the browser's own "display: none", and any
 * display declared here replaces it - so the window stood on the page from the
 * first paint, and the × did nothing because there was no open dialog to
 * close. The [open] attribute is what the browser sets, so the flex column
 * exists exactly while the window does. */
.legal-dialog[open] {
	display: flex;
	flex-direction: column;
}

.legal-dialog-body {
	overflow-y: auto;
	text-align: left;
}

/* The link inside a sentence: a button, because pressing it opens a window
   rather than going to an address - and it has to read as the word it is,
   not as a control. */
.legal-open {
	padding: 0;
	background: none;
	border: 0;
	border-radius: 0;
	font: inherit;
	color: var(--accent);
	text-decoration: underline;
	text-underline-offset: 0.2em;
	cursor: pointer;
}

.legal-open:hover,
.legal-open:focus-visible {
	background: none;
	color: var(--accent-soft);
}

/* The two lines the law asks for, at the foot of the form.
   No box around them: every other block on this form is a box because it holds
   fields with a heading over them, and a bordered rectangle with no heading
   reads as one somebody forgot to finish. These two are a sentence and a tick
   between the last block and the button. */
.checkout-consent {
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
	margin: 0 0 1.1rem;
}

.checkout-terms {
	display: flex;
	align-items: flex-start;
	gap: 0.6rem;
	font-size: 0.95rem;
	line-height: 1.45;
	cursor: pointer;
}

.checkout-terms input {
	width: auto;
	margin-top: 0.15rem;
	accent-color: var(--accent);
}

/* The mark a refused order leaves on the box. The same red the fields get,
   on the box itself rather than on the sentence beside it. */
.checkout-terms input.chat-input-bad {
	outline: 2px solid #ff6b57;
	outline-offset: 2px;
}

.checkout-privacy {
	margin: 0;
	font-size: 0.85rem;
	opacity: 0.75;
	line-height: 1.5;
}

.login-dialog-title {
	/* Room for the × beside it, so a long title does not run under the
	   button - the terms' own title shares this dialog. */
	padding-right: 2rem;
	margin: 0 0 1.2rem;
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.5rem;
	letter-spacing: 0.06em;
}

/* The options are separated by a rule rather than by boxes: a box inside a box is
   heavy, and a rule is enough to say these are different ways in. */
.login-option + .login-option {
	margin-top: 1.2rem;
	padding-top: 1.2rem;
	border-top: 1px solid rgba(255, 255, 255, 0.14);
}

.login-option-title {
	margin: 0 0 0.4rem;
	font-weight: 700;
	font-size: 0.95rem;
}

.login-option-note {
	margin: 0.5rem 0 0;
	font-size: 0.8rem;
	line-height: 1.45;
	color: rgba(244, 239, 230, 0.65);
}

.login-dialog .login-option-demo input {
	padding: 0.45rem 0.6rem;
	border-radius: 9px;
	font-size: 0.85rem;
}

.login-dialog .login-option-demo button {
	margin: 0;
	padding: 0.45rem 0.9rem;
	font-size: 0.78rem;
	white-space: nowrap;
}

.login-dialog-error {
	margin: 1rem 0 0;
	color: #ff8a7a;
	font-weight: 700;
	font-size: 0.85rem;
}

/* --- Talli-siru ----------------------------------------------------------- */

/* The same scale as the universal chip, because they share a row and are of the
   same kind: choices trailing the bike chip. The colour differs, because this is
   an action rather than a filter - the universal chip changes what the shop
   shows, this saves something to the profile. */
/* The break that puts the garage chip on a line of its own. Hidden when the
   chip is - an empty line under the bike would be a gap with no cause. */
.chip-row-break {
	flex: 0 0 100%;
	height: 0;
}

/* No chip, no break: an empty line under the bike would be a gap with no
   cause. The cart then stays on the first line, where it was. */
.finder-cart-row:has(.garage-chip.hidden) .chip-row-break {
	display: none;
}

/* On the second line, under the bike and beside the cart.
   
   It was the fourth chip on a row that already carried the bike, UNIVERSAALIT
   MUKANA and LISÄÄ ILMAN VUOSIMERKINTÄÄ - and it is not one of those: the three
   narrow what the shop shows, this one saves a bike to the garage. Read in a
   row they looked like four settings of the same kind. */
.garage-chip {
	flex: 0 0 auto;
	/* The same indent the chips above it have: it left .bike-finder-wrap to share
	   a line with the cart, and the indent lives on that box. Without this it
	   started at the row's edge and the bike above it did not. */
	margin-left: var(--brand-group-shift);
	display: inline-flex;
	align-items: center;
	padding: 0.5rem 0.9rem;
	border-radius: 999px;
	background: none;
	border: 1px solid rgba(255, 255, 255, 0.45);
	color: #f4efe6;
	font: inherit;
	font-size: 0.82rem;
	font-weight: 700;
	letter-spacing: 0.05em;
	cursor: pointer;
}

.garage-chip:hover,
.garage-chip:focus-visible {
	background: rgba(255, 255, 255, 0.12);
	border-color: #fff;
}

/* The saved state: the same chip, but as status rather than a button. Dimmer and
   with no pointer, so it does not invite a click - it is already done. */
.garage-chip-saved,
.garage-chip-saved:hover {
	background: none;
	border-color: rgba(255, 255, 255, 0.22);
	color: rgba(244, 239, 230, 0.6);
	cursor: default;
}

/* --- Etusivun tarjoukset -------------------------------------------------- */

/* One centred, several side by side.
   With one, the emphasis is the whole point: it is the shop's pick and it should
   look like one. Several are alternatives to each other, so an evenly sized row is
   the right shape. */
.front-offers {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: stretch;
	gap: 0.9rem;
	width: 100%;
	margin-top: 0.4rem;
}

/* A neutral grey background rather than orange.
   An orange ground AND orange text competed with each other: once the ground is
   tinted, the text no longer stands out from it but melts in. A grey ground leaves
   the orange text as the only coloured thing on the card, and that is what catches
   the eye. */
.front-offer {
	display: flex;
	align-items: center;
	gap: 0.7rem;
	flex: 0 1 19rem;
	padding: 0.8rem 0.9rem 0.8rem 0.8rem;
	border: 1px solid rgba(255, 255, 255, 0.16);
	border-radius: 14px;
	background: rgba(255, 255, 255, 0.07);
	color: #f4efe6;
}

/* The link is inside the card rather than being the card: an add-to-cart button
   cannot live inside a link. The link takes the remaining space, so the button ends
   up at the right edge. */
.front-offer-link {
	display: flex;
	align-items: center;
	gap: 0.9rem;
	flex: 1;
	min-width: 0;
	color: inherit;
	text-decoration: none;
}

.front-offer:hover {
	background: rgba(255, 255, 255, 0.11);
	border-color: rgba(255, 255, 255, 0.32);
}

.front-offer-link:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 3px;
	border-radius: 10px;
}

/* A single pick: larger, but the SAME orientation - image to the left, text to
   the right. A vertical, centred version made one pick a different-looking
   element from the same picks shown as two. */
.front-offers-single .front-offer {
	flex: 0 1 auto;
	gap: 1.2rem;
	padding: 1.1rem 1.2rem 1.1rem 1.1rem;
	max-width: min(32rem, 100%);
}

.front-offers-single .front-offer-link {
	gap: 1.4rem;
}

.front-offer-image {
	width: 4rem;
	height: 4rem;
	flex: none;
	object-fit: contain;
	border-radius: 10px;
	background: #fffdf8;
}

.front-offers-single .front-offer-image {
	width: 7rem;
	height: 7rem;
}

.front-offer-image-empty {
	background: rgba(255, 255, 255, 0.08);
}

.front-offer-body {
	display: flex;
	flex-direction: column;
	gap: 0.25rem;
	min-width: 0;
}

.front-offers-single .front-offer-body {
	gap: 0.35rem;
}

/* The discount at the head of the row and in the accent colour. Orange text rather
   than an orange tile: the tile was a second coloured surface on a grey card, and
   two highlights on one card take the force out of each other. */
.front-offer-badge {
	align-self: flex-start;
	color: var(--accent);
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.15rem;
	letter-spacing: 0.06em;
	line-height: 1;
	white-space: nowrap;
}

.front-offers-single .front-offer-badge {
	font-size: 1.6rem;
}

.front-offer-title {
	font-size: 0.85rem;
	line-height: 1.3;
	overflow-wrap: anywhere;
}

.front-offers-single .front-offer-title {
	font-size: 1rem;
}

.front-offer-price {
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.15rem;
	letter-spacing: 0.03em;
	color: #fff;
}

/* The shop's own text below the price: it is extra information rather than a
   price, so it is smaller and dimmer than the rest of the card. */
.front-offer-text {
	font-size: 0.8rem;
	line-height: 1.35;
	color: rgba(244, 239, 230, 0.7);
}

.front-offers-single .front-offer-price {
	font-size: 1.5rem;
}

/* The old price struck through: the difference is what makes an offer an
   offer. */
.front-offer-price s {
	opacity: 0.6;
	font-size: 0.85em;
	margin-right: 0.2rem;
}

/* --- Admin: tarjoukset ---------------------------------------------------- */

.admin-offer-form {
	margin-top: 1.2rem;
}

.admin-offer-list {
	list-style: none;
	margin: 0.7rem 0 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	max-height: 30rem;
	overflow-y: auto;
}

.admin-offer-item {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	align-items: center;
	gap: 0.6rem;
	padding: 0.7rem 0;
	border-top: 1px solid rgba(255, 255, 255, 0.12);
}

.admin-offer-info p {
	margin: 0;
}

.admin-offer-sku {
	font-weight: 700;
	color: var(--accent);
	font-size: 0.88rem;
}

.admin-offer-title {
	font-size: 0.82rem;
	line-height: 1.3;
}

.admin-offer-meta {
	margin-top: 0.15rem !important;
	font-size: 0.78rem;
	color: rgba(244, 239, 230, 0.65);
}

/* Two blocks in the right column: the text and the picks. They are different
   things, so they get boxes of their own - a single box holding both would read as
   one setting. */
.admin-offer-side {
	display: flex;
	flex-direction: column;
	gap: 0.9rem;
	min-width: 0;
}

.admin-promo-form {
	margin-top: 0.9rem;
}

/* A preview of the product before it becomes a pick. The admin sees which part it
   is before it goes onto the front page - an article number alone does not say. */
.admin-offer-preview {
	display: flex;
	align-items: center;
	gap: 0.9rem;
	margin-top: 1.1rem;
	padding: 0.8rem;
	border: 1px solid rgba(255, 255, 255, 0.16);
	border-radius: 12px;
	background: rgba(255, 255, 255, 0.05);
}

.admin-offer-preview-image {
	width: 4.5rem;
	height: 4.5rem;
	flex: none;
	object-fit: contain;
	border-radius: 8px;
	background: rgba(255, 255, 255, 0.9);
}

.admin-offer-preview-body {
	min-width: 0;
}

.admin-offer-preview-body p {
	margin: 0;
}

/* Edit and delete on the SAME row. They used to be stacked, which made a pile two
   words wide - side by side they read as two options. As text links, because they
   are the row's actions rather than the block's calls to action. */
.admin-offer-actions {
	display: flex;
	align-items: center;
	gap: 0.9rem;
	white-space: nowrap;
}

/* The text block after the picks in the same box: both are things the admin sets,
   so they belong in the same column.

   The rule sits BELOW the heading rather than at the top of the block. At the top
   it separated the block from the previous form, but left the heading detached from
   its content - below the heading it makes the heading the block's heading. */
/* The explanation for a disabled import button. Beside the button rather than in
   its place: the button says the action exists, the text says why it is not
   available right now. */
.admin-import-disabled {
	margin-top: 0.5rem;
	font-size: 0.85rem;
	opacity: 0.7;
}

.admin-promo-block {
	margin-top: 1.6rem;
}

/* The selector starts from .admin-panel and comes later than ".admin-panel
   .eyebrow" (the same specificity, 0,2,0), so the margin-top is overridden here by
   order. */
.admin-panel .admin-promo-block > .eyebrow {
	margin-top: 0;
	padding-bottom: 0.7rem;
	border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}

/* A campaign row: a checkbox followed by a sentence with selects sitting between
   its words. flex + wrap rather than grid, because the sentence's parts vary in
   size from row to row - in the first the select is at the start, in the second in
   the middle, and the third has two of them.

   The checkbox's appearance comes from the shared .admin-checkbox-row rule: the
   global "input { width: 100% }" and its border/background stretch the box into a
   column-wide white rectangle, and only appearance: checkbox brings the native box
   back. The same trap has caught this code before, so there is one rule rather than
   two. */
/* The warning stands out from the description but does not shout: it is a fact the
   dealer needs while choosing, not an error. */
.admin-promo-warning {
	margin-top: 0.6rem;
	padding-left: 0.7rem;
	border-left: 2px solid var(--accent);
	color: var(--ink);
	opacity: 0.8;
}

.admin-promo-option {
	display: flex;
	align-items: flex-start;
	gap: 0.7rem;
	margin-top: 0.9rem;
}

/* A campaign that exists but cannot be chosen. Dimmed rather than hidden: it
   was specified and the dealer asked for it, so the row says where it will go,
   and the note under it says why it is not there yet. flex-wrap lets the note
   take a line of its own under the sentence. */
.admin-promo-off {
	flex-wrap: wrap;
	opacity: 0.55;
}

.admin-promo-note {
	flex-basis: 100%;
	margin: 0.15rem 0 0 1.9rem;
	font-size: 0.85rem;
}

/* Alignment only: the box's appearance comes from the .admin-checkbox-row rule
   this selector is attached to. Aligned to the first text line's height rather than
   the box's top edge, because the sentence can run onto two lines and the checkbox
   belongs at its start. */
.admin-promo-option input[type="checkbox"] {
	flex: none;
	margin-top: 0.3rem;
}

/* One campaign = one row. The row does not break between words (nowrap), because
   a broken sentence looked like two separate options - the menu stayed on the
   first line and the rest of the sentence went to the second.

   A smaller font, so the longest sentence ("... (vaatii sisäänkirjautumisen
   Google-tilillä)") fits the column. overflow-x as a fallback: on a very narrow
   screen the row scrolls rather than breaking, so no text is lost. */
.admin-promo-line {
	display: flex;
	flex-wrap: nowrap;
	align-items: center;
	gap: 0.4rem;
	min-width: 0;
	white-space: nowrap;
	font-size: 0.86rem;
	line-height: 1.5;
	cursor: pointer;
	overflow-x: auto;
}

/* A select inside a sentence: narrow and sized to its content rather than the
   column's width like .admin-select. The width comes from the content, so "5" does
   not take the same room as "400". */
.admin-promo-line select {
	width: auto;
	border: 1px solid rgba(24, 21, 17, 0.18);
	background: rgba(255, 255, 255, 0.92);
	border-radius: 8px;
	padding: 0.3rem 0.4rem;
	font: inherit;
	font-weight: 700;
	color: var(--ink);
	cursor: pointer;
}

/* The preview's text looks the way it looks on the front page: the same accent
   colour and the same font, in a smaller size. */
.admin-front-promo {
	margin: 1rem 0 0;
	color: var(--accent);
	font-family: "Bebas Neue", sans-serif;
	font-size: 1rem;
	letter-spacing: 0.04em;
	text-align: center;
}

/* To the left, on the same alignment as the "Nostoja ei ole." text: both are empty
         states of the same preview, and centring one of them made it read from a
         different level than the other. The campaign text itself stays centred, because
         it looks the way it looks on the front page. */
.admin-front-promo-empty {
	opacity: 0.55;
}

/* The add-to-cart button on a pick: a round icon button at the card's right edge.
   An icon rather than text, because the card is narrow and text would take the room
   from the price - and adding to the cart is such an ordinary action that an icon
   is enough. */
.front-offer-cart {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: none;
	width: 2.6rem;
	height: 2.6rem;
	padding: 0;
	border-radius: 999px;
}

.front-offers-single .front-offer-cart {
	width: 3rem;
	height: 3rem;
}

.front-offer-cart svg {
	width: 1.3rem;
	height: 1.3rem;
	fill: none;
	stroke: #fff;
	stroke-width: 2;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.front-offer-cart circle {
	fill: #fff;
	stroke: none;
}

/* Two icons in the button, one visible at a time. The swap is by class rather
   than by writing content: textContent would destroy the SVG for good, and the
   first click would leave the button empty. */
.front-offer-cart-check {
	display: none;
}

.front-offer-cart.front-cart-btn-added .front-offer-cart-icon {
	display: none;
}

.front-offer-cart.front-cart-btn-added .front-offer-cart-check {
	display: block;
}

/* The green confirmation. !important, because .front-cart-btn-added is defined
   earlier in the file at the same specificity and the base state's gradient comes
   from the button rule - without this the colour would depend on the order. */
.front-offer-cart.front-cart-btn-added {
	background: linear-gradient(140deg, #3f9c46, #2b6e31) !important;
}

/* The text and its actions on the same row, as with the picks. */
.admin-front-promo-row {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	align-items: center;
	gap: 0.6rem;
	margin-top: 1rem;
	padding-top: 0.9rem;
	border-top: 1px solid rgba(255, 255, 255, 0.12);
}

.admin-front-promo-row .admin-front-promo {
	margin: 0;
	text-align: left;
}

/* A dead image address. The replacement image is already a neutral tile, so this
   only makes sure the background colour does not show through it and that
   object-fit does not crop it. */
img.image-missing {
	object-fit: cover;
	background: none;
}

/* The old price struck through. The same look everywhere a price appears: the
   product card, the product page, the cart and the checkout summary - a different
   look in each would leave the customer guessing which number gets charged. */
.front-product-price s,
.front-detail-price s,
.cart-meta s,
.cart-line s,
.cart-total s,
.checkout-line-meta s,
.checkout-line-sum s {
	opacity: 0.55;
	font-size: 0.85em;
	font-weight: 400;
	margin-right: 0.15rem;
}

/* The saving on a row of its own below the total. It is the same information as
   the struck-through figure, but in euros - and euros are what the customer
   compares, not percentages. */
.cart-saving {
	margin: 0.3rem 0 0;
	text-align: right;
	color: var(--accent-soft);
	font-weight: 700;
	font-size: 0.9rem;
}

/* The goods and the shipping on rows of their own above the grand total. Shipping
   is part of what the customer pays, and a cart total without it read as a promise
   the checkout did not keep. */
.cart-sums {
	justify-self: stretch;
	width: 100%;
	margin: 1rem 0 0;
	display: flex;
	flex-direction: column;
	gap: 0.35rem;
	font-size: 0.95rem;
}

/* The discount row in the accent colour: it is the row the customer is looking
   for, and without the colour it drowns between the goods and the shipping. */
.cart-discount-row dd,
.checkout-discount-row dd {
	color: var(--accent);
	font-weight: 700;
}

/* Free shipping in words rather than as 0,00. Zero euros on a "Postitus" row
   looks like a fault - the word says it is a campaign. */
.checkout-free {
	color: var(--accent);
	font-weight: 700;
	letter-spacing: 0.04em;
}

/* How much is missing from a campaign's threshold. Dim and small: it is a nudge
   rather than a figure, and it must not compete with the grand total. */
.cart-hint {
	margin: 0.5rem 0 0;
	color: var(--muted);
	font-size: 0.85rem;
	text-align: right;
}

.cart-sums div {
	display: flex;
	justify-content: space-between;
	gap: 1rem;
}

.cart-sums dt,
.cart-sums dd {
	margin: 0;
}

.cart-sums dd {
	font-variant-numeric: tabular-nums;
}

/* The rule above the grand total moved to the total rows, so it no longer
   separates the total from the product lines but from the goods and the
   shipping. */
.cart-total {
	margin-top: 0.6rem !important;
	padding-top: 0.7rem !important;
}

/* The return link on the shop side. There were three of these unstyled, so they
   took the browser's default colours - blue, and purple once visited. After a
   payment the link is always visited, so it showed purple in exactly the place
   where it is the page's only action.
   The accent colour and an underline that strengthens on hover: it is an ordinary
   text link rather than a button, so it may look like a link. */
.front-back-link {
	color: var(--accent);
	font-weight: 700;
	text-decoration: underline;
	text-decoration-color: rgba(242, 106, 27, 0.5);
	text-decoration-thickness: 2px;
	text-underline-offset: 0.25em;
}

.front-back-link:visited {
	color: var(--accent);
}

.front-back-link:hover,
.front-back-link:focus-visible {
	color: var(--accent-soft);
	text-decoration-color: var(--accent-soft);
}


/* --- Phones and narrow windows ------------------------------------------------------
 *
 * Everything below here is the mobile layout. It sits at the end of the file on
 * purpose: these rules have the same specificity as the desktop ones they
 * override, so they win by order alone and nothing above needs a mobile branch.
 *
 * Three things change, and they are the three that break a fixed-width page on a
 * phone:
 *
 *   1. What floats comes back into the flow. The profile, the login button, the
 *      search results and the supplier selector are positioned against the
 *      1120 px shell, which does not exist here.
 *   2. Every multi-column grid folds into one, except the product grid, which
 *      goes to two - a 165 px card is still a readable card, and one per row
 *      would make a category page an endless scroll.
 *   3. Hover stops being an interaction. The chapter menu's submenu opens on
 *      hover; on a touch screen a tap both opens it and follows the link, so the
 *      panel flashes over the page it is leaving. The chapter page already lists
 *      the same subcategories, so the panel is simply hidden.
 */
@media (max-width: 1000px) {
	/* Three columns before the layout folds: at this width five cards leave the
	   title no room and the price wraps under the card. */
	.front-products {
		grid-template-columns: repeat(3, minmax(0, 1fr));
	}
}

@media (max-width: 900px) {
	/* The background stands still while the content scrolls over it.
	 *
	 * Three ways to do this and only the third works on a phone.
	 * background-attachment: fixed is the obvious one, and Safari re-composites
	 * the image on every scroll frame - the page drags. scroll is smooth but
	 * the photograph slides away with the content, so the whole screen reads as
	 * moving rather than the page moving over it. A fixed layer of its own is
	 * painted once and never again: the scroll touches only what is above it.
	 *
	 * lvh rather than vh: on iOS the URL bar hides as you scroll and the
	 * viewport grows, which would rescale a 100vh layer mid-scroll - the exact
	 * movement this is here to remove. lvh is the largest state, so the layer is
	 * sized for it from the start. vh first as the fallback for browsers that do
	 * not know lvh.
	 */
	body {
		min-width: 0;
		background-image: none;
	}

	/* The page moves in one direction only.
	 *
	 * A phone pans sideways as soon as anything is a pixel wider than the
	 * screen, and the culprit is usually invisible: a decorative layer, a
	 * dropdown positioned against an edge that is not there any more, a chip
	 * that cannot wrap. The page then drifts under the thumb on every vertical
	 * scroll, which reads as the whole site being loose.
	 *
	 * hidden rather than clip, and that order was learned the hard way: clip is
	 * the tidier property - it does not make the element a scroll container - but
	 * on the root element it is not reliably propagated to the viewport, and the
	 * page still panned on the phone with clip in place. hidden propagates
	 * everywhere. Its one cost is that a descendant's position: sticky stops
	 * working, and the only sticky rule in this file is .admin-loaded, which is
	 * an admin header that does not matter at this width.
	 *
	 * max-width alongside it: hidden clips the paint, max-width keeps a child
	 * from being laid out wider than the screen in the first place.
	 *
	 * This is a floor, not a diagnosis. If something in the shop turns out to be
	 * genuinely wider than a phone, this hides it instead of fixing it - so
	 * anything cut off at the right edge is a bug to chase, not a setting to
	 * adjust here.
	 */
	html,
	body {
		max-width: 100%;
		overflow-x: hidden;
	}

	body::before {
		content: "";
		position: fixed;
		top: 0;
		left: 0;
		width: 100%;
		height: 100vh;
		height: 100lvh;
		z-index: -1;
		background-image:
			linear-gradient(rgba(8, 8, 8, 0.78), rgba(8, 8, 8, 0.9)),
			url("/assets/bg.jpg?v=4");
		background-size: cover;
		background-position: center 42%;
		background-repeat: no-repeat;
		pointer-events: none;
	}

	/* Safari on iOS zooms the page when a field with text under 16 px takes
	   focus, and it does not zoom back out. A zoomed page can be dragged in every
	   direction, which is what "the shop moves sideways" turned out to be: the
	   page was still until the question box was opened, because opening it moves
	   the cursor into the message field.
	 *
	 * !important, and it is the right tool rather than a shortcut. The sizes that
	 * have to be overridden come from component rules:
	 *
	 *     .chat-form input                14.7px   specificity 0,1,1
	 *     .checkout-field input           15.2px               0,1,1
	 *     .bike-finder-select             15.2px               0,1,0
	 *     .topbar-login-form input        12.5px               0,1,1
	 *
	 * A bare element selector is 0,0,1 and loses to every one of them no matter
	 * where it sits in the file - the first version of this rule was in exactly
	 * this place and never applied to a single field. The alternative is listing
	 * every component selector here, and that list goes stale the day someone
	 * adds a form.
	 *
	 * 16px rather than 1rem: this has to hold even if the root size is changed.
	 */
	input,
	select,
	textarea {
		font-size: 16px !important;
	}

	/* --- Header ------------------------------------------------------------ */

	.topbar {
		flex-direction: column;
		align-items: stretch;
	}

	.topbar-brand h1 {
		font-size: clamp(2.2rem, 11vw, 3.2rem);
	}

	/* The search field and the menu stack. On the desktop they share a row and
	   the menu sits at the right edge; at this width the menu alone needs three
	   rows, and beside the field it would push the field to a thumb's width. */
	.search-row {
		flex-direction: column;
		align-items: stretch;
		gap: 0.75rem;
		margin-top: 1.4rem;
	}

	#searchForm {
		flex: 0 0 auto;
		width: 100%;
		max-width: none;
	}

	.site-nav {
		align-self: stretch;
		justify-content: flex-start;
		gap: 0.3rem 0.9rem;
		/* Indented to the same line as the words below it. The glass box pads
		   its contents by 0.8rem and the search field by about the same, so the
		   menu flush against the screen's edge was the one row of text on the
		   page starting further left than everything else. Both its rows move,
		   because both are the menu. */
		padding-left: 0.8rem;
	}

	/* Bigger on a phone than on a desktop, not smaller.
	   These were 0.82rem, set when the row had to squeeze onto one line beside
	   the search field. It wraps onto its own lines here, so there is room -
	   and a menu is read at arm's length and pressed with a thumb, which is
	   the opposite of the case for making it small. */
	.site-nav-link {
		font-size: 1.08rem;
	}

	.site-nav-shop {
		font-size: 1.28rem;
		/* Less here: the row wraps on a phone, and a wide gap would push Huolto
		   onto a line of its own. */
		margin-right: 0.6rem;
	}

	/* The login button and the profile are positioned against the shell's right
	   edge, which is the window's edge here - they would sit on top of the shop
	   name. Both come back into the flow as a full-width row.
	   The profile keeps position: relative rather than static, because its menu
	   is an absolutely positioned child and anchors to it. */
	.topbar-login-btn {
		position: static;
		align-self: flex-end;
		margin-top: 0.8rem;
	}

	.profile-card {
		position: static;
		width: auto;
		height: auto;
		max-height: none;
		margin-top: 0.8rem;
	}

	.profile-badge {
		position: relative;
		top: auto;
		right: auto;
		width: 100%;
		min-width: 0;
		max-width: none;
	}

	/* The results card is fixed and centred with a transform on the desktop,
	   which puts it over the shop on a phone. In the flow it is just the next
	   block on the page. */
	.results-card {
		position: static;
		transform: none;
		width: 100%;
		max-height: none;
		margin: 1rem 0 0;
	}

	.search-form,
	.results-head-row {
		grid-template-columns: minmax(0, 1fr) auto;
	}

	/* Room under the last thing on the page for the floating bubble. It is fixed
	   to the bottom right corner, so without this it sits on top of whatever the
	   page ends with - and on a phone that is VAHVISTA TILAUS or NÄYTÄ LISÄÄ,
	   i.e. the one button the customer was reaching for. The value is the
	   bubble's own height (3.6rem at this width) plus its margin and a gap. */
	.app-shell {
		padding-bottom: 5.5rem;
	}

	/* --- Shop --------------------------------------------------------------- */

	.glass-box {
		min-height: 0;
		padding: 0.9rem 0.8rem;
	}

	/* The supplier selector is absolutely positioned in the box's top right
	   corner, where on a phone it lands on the chapter menu. In the flow it is a
	   row of its own above the content. */
	.front-suppliers {
		position: static;
		justify-content: flex-start;
		margin-bottom: 0.7rem;
	}

	.front-panel {
		padding-top: 0;
	}

	/* The bike on its own row, the cart under it in the right corner.
	 *
	 * Two things went wrong here in turn and the fix is the pair of them. The row
	 * wrapped, which is right - a 390 px screen has no room for a long label and
	 * the cart side by side - but justify-content: space-between does nothing to
	 * a line holding one item, so the cart landed against the LEFT edge. Making
	 * the row nowrap put it right but at the wrong height: level with the bike
	 * button, floating in the gap above the box.
	 *
	 * So: the bike takes the full width, the cart wraps beneath it, and
	 * margin-left: auto pins it right. It then sits just above the box's top
	 * right corner, which is where a cart belongs - next to the thing it fills.
	 */
	.finder-cart-row {
		flex-wrap: wrap;
		align-items: center;
		gap: 0.5rem;
		margin: 1.2rem 0 0.5rem;
	}

	.finder-cart-row .bike-finder-wrap {
		flex: 1 1 100%;
		min-width: 0;
		flex-wrap: wrap;
	}

	/* The label wraps instead of forcing the row wider. On the desktop it is one
	   line and nowrap is right; at 390px it is most of the screen. */
	.bike-finder-btn {
		white-space: normal;
		text-align: left;
	}

	.finder-cart-row .cart-link {
		flex: none;
		margin-left: auto;
	}

	/* The cart is lifted into the topbar's area with a negative margin on the
	   desktop. Here the topbar is above it in the flow, so the lift would pull it
	   over the search field. */
	.cart-link {
		margin-top: 0;
	}

	/* Three selects and a button in a row need about 600 px. Stacked, each one
	   gets the full width and stays a comfortable tap target. */
	.bike-finder-row {
		flex-wrap: wrap;
		justify-content: center;
	}

	.bike-finder-row .bike-finder-select {
		flex: 1 1 100%;
	}

	.bike-finder-row .bike-finder-btn {
		flex: 1 1 100%;
		min-height: 44px;
	}

	/* VALITSE centred rather than stretched. Full width it read as another field
	   in the stack of selects above it; centred and sized to its own text it
	   reads as the thing that ends the choosing. */
	#bikeSelectBtn {
		flex: 0 0 auto;
		min-width: 12rem;
		min-height: 44px;
		margin: 0.4rem auto 0;
	}

	/* Room under the button. The panel's own bottom padding was written for a
	   desktop row where the button sits beside the selects; stacked, the button
	   is the last thing in the box and sat against its edge. */
	.bike-finder {
		max-height: 60vh;
		padding: 1.1rem 1rem 2rem;
	}

	.front-menu {
		gap: 0.3rem 1.2rem;
	}

	/* The chapter menu is the shop's main navigation and on a phone it is most of
	   what the first screen holds - so it is larger here than the desktop's
	   proportion would give, not smaller. */
	.front-category-lead {
		font-size: 2.05rem;
	}

	.front-category-big {
		font-size: 1.75rem;
	}

	.front-category-small {
		font-size: 1.28rem;
	}

	/* --- The selected bike -------------------------------------------------- */
	/*
	 * Two lines and a row of its own.
	 *
	 * On the desktop the chip is one pill: icon, HARLEY-DAVIDSON LOW RIDER S,
	 * FXLRS · 2020 · RAJATTU and the cross, side by side. That is around 420 px of
	 * text, so on a 390 px screen it either pushed the row wider than the screen
	 * or squeezed the name until the model was unreadable - and it covered the
	 * universal button beside it.
	 *
	 * The name on the first line and the code, year and RAJATTU on the second.
	 * The icon spans both. The cross stays on the right, vertically centred,
	 * because that is where it is on the desktop and it is the one control here
	 * that has to stay findable.
	 */
	.bike-chip {
		flex: 1 1 100%;
		align-items: center;
	}

	.bike-chip-open {
		flex: 1 1 auto;
		min-width: 0;
		display: grid;
		grid-template-columns: auto minmax(0, 1fr);
		gap: 0 0.5rem;
		padding: 0.4rem 0.7rem;
		text-align: left;
		font-size: 0.82rem;
	}

	.bike-chip-open .bike-finder-icon {
		grid-row: 1 / span 2;
		align-self: center;
		width: 1.15rem;
		height: 1.15rem;
	}

	.bike-chip-name {
		grid-column: 2;
		line-height: 1.25;
	}

	.bike-chip-meta {
		grid-column: 2;
		font-size: 0.7rem;
		line-height: 1.2;
	}

	/* Below the bike rather than beside it. Beside it the chip took the whole row
	   and this button was pushed out of sight - it is the control that says
	   whether universal parts are in the filtered shop, so losing it loses the
	   answer. */
	.universal-chip,
	.garage-chip {
		flex: 0 0 auto;
		font-size: 0.75rem;
		padding: 0.4rem 0.75rem;
	}

	/* The hover submenu does not work on a touch screen: a tap opens it and
	   follows the link at the same time, so it flashes over the page it is
	   leaving. The chapter page lists the same subcategories, which is where the
	   tap goes anyway. */
	.front-category-item:hover .front-subcategories,
	.front-subcategories {
		display: none;
	}

	.front-products {
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: 0.7rem;
	}

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

	/* The heading and the filters share a row that is sized for a 1120 px box:
	   the right column alone asks for at least 300 px and pushes itself to the
	   right edge with margin-left: auto. At this width it takes the whole row
	   below the heading instead. */
	.front-head-right {
		flex: 1 1 100%;
		margin-left: 0;
	}

	/* --- Cart --------------------------------------------------------------- */

	/* Five columns become three, and the row becomes two lines: the image spans
	   both, the title and the delete button are on the first, the quantity and
	   the line total on the second. Every cell is placed explicitly - left to
	   implicit placement the quantity stepper would land beside the title and
	   push the price off the row. */
	.cart-row {
		grid-template-columns: 56px minmax(0, 1fr) auto;
		gap: 0.45rem 0.7rem;
		align-items: start;
	}

	.cart-image {
		width: 56px;
		grid-column: 1;
		grid-row: 1 / span 2;
	}

	.cart-info {
		grid-column: 2;
		grid-row: 1;
	}

	.cart-remove {
		grid-column: 3;
		grid-row: 1;
		justify-self: end;
	}

	.cart-qty {
		grid-column: 2;
		grid-row: 2;
		justify-self: start;
	}

	.cart-line {
		grid-column: 3;
		grid-row: 2;
		align-self: center;
		min-width: 0;
	}

	.cart-actions {
		flex-direction: column-reverse;
		align-items: stretch;
		/* Wide, because these two are not a pair. Stacked 0.7rem apart they
		   read as one control with two halves, and the half underneath empties
		   the cart. */
		gap: 1.8rem;
	}

	.cart-actions button {
		width: 100%;
		min-height: 44px;
		padding: 0.72rem 1rem;
	}

	/* Emptying the cart is a word here, not a second orange pill. Two
	   full-width buttons one above the other offer two equal choices, and
	   these are not equal: one continues the order, the other throws it away.
	   It keeps a 44 px target, because it is still something to hit with a
	   thumb - deliberately, and not by accident on the way to KASSALLE. */
	.cart-actions .ghost {
		width: auto;
		align-self: center;
		min-height: 44px;
		padding: 0.4rem 0.8rem;
		background: none;
		color: var(--muted);
		font-weight: 500;
		text-decoration: underline;
		text-underline-offset: 0.25em;
	}

	/* --- Checkout and returns ----------------------------------------------- */

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

	/* What is being bought, before the fields that ask who is buying it.
	   Side by side the summary is simply there while the form is filled in;
	   stacked it would land below the last field, and the customer would fill
	   in an address, a postcode and a phone number before seeing the total
	   they are about to confirm. The markup keeps the form first, because that
	   is the order to read and to tab through on a wide screen. */
	.checkout-summary {
		order: -1;
	}

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

	#checkoutSubmit {
		width: 100%;
		min-width: 0;
		min-height: 48px;
	}

	.return-line {
		flex-wrap: wrap;
	}

	/* --- Question panel ------------------------------------------------------ */

	.chat-widget {
		right: 0.75rem;
		bottom: 0.75rem;
	}

	/* Full width minus the bubble's own margins. The desktop width (33rem) is
	   wider than a phone, and the panel is anchored to the right edge, so the
	   overflow would go off the left side of the screen. */
	.chat-panel {
		width: calc(100vw - 1.5rem);
	}

	/* The height is measured in svh, not vh, and that is the whole fix for a bug
	 * that had no way out but reloading the page.
	 *
	 * On iOS 100vh is the LARGE viewport - the height the page would have if the
	 * address bar were hidden - and it stays that even while the bar is showing.
	 * The panel is anchored by its bottom to the bubble and grows upwards, so a
	 * height computed from 100vh pushed its top, and the close button with it,
	 * above the visible area. The bubble went the same way, so neither way of
	 * closing the panel could be reached.
	 *
	 * svh is the SMALL viewport: the height with the browser's own chrome in
	 * place. Sized by it the panel always fits what is actually on screen. The vh
	 * line stays first for browsers that do not know svh (below iOS 15.4), where
	 * it is the old behaviour rather than no height at all.
	 */
	.chat-panel,
	.admin-inbox-panel {
		max-height: calc(100vh - 0.75rem - clamp(3.6rem, 6vw, 4.6rem)
		                 - var(--bubble-gap) - 0.75rem);
		max-height: calc(100svh - 0.75rem - clamp(3.6rem, 6vw, 4.6rem)
		                 - var(--bubble-gap) - 0.75rem);
	}

	/* Dragging is off below this width (see startChatDrag): the panel already
	   fills the screen, and a drag would only move it off it. The grab cursor
	   would be a promise nothing keeps. */
	.chat-title {
		cursor: default;
	}

	.admin-inbox-panel {
		width: calc(100vw - 1.5rem);
	}

	/* --- Admin --------------------------------------------------------------- */

	.admin-panel .admin-grid,
	.admin-panel .admin-grid-edit,
	.admin-panel .admin-grid-orders,
	.admin-panel .admin-product-search,
	.admin-form,
	.admin-summary-row {
		grid-template-columns: minmax(0, 1fr);
	}

	.admin-nav {
		flex-wrap: wrap;
	}
}

@media (max-width: 480px) {
	/* The cards keep two columns, but the grid's own gap is the only room the
	   phone has left to give. */
	.front-products {
		gap: 0.5rem;
	}

	.front-title {
		font-size: 1.25rem;
	}

	/* A 64px tile plus its padding leaves under half the row for the title at
	   this width, so a pick takes the full row rather than sharing it. */
	.front-offer {
		flex: 1 1 100%;
	}
}


/* --- Footer -------------------------------------------------------------------------
 *
 * The links that are never browsed to and always have to be findable: terms,
 * returns, contact. Returns used to be a top level menu item, which put a
 * customer service function beside the shop's business areas and in front of
 * every visitor who has bought nothing.
 *
 * Inside the box, at its bottom edge, centred. The shop has no page furniture
 * outside the box - a row of links floating under it read as something that had
 * fallen off. Front page only: there the panel leaves room at the bottom by
 * design, while a category page ends in products and a cart in its own total.
 *
 * Quiet on purpose. A footer that competes with the shop is a footer in the
 * wrong place - this one is for the person already looking for it.
 */
.site-footer {
	display: none;
}

.glass-box-home .site-footer {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 0.5rem 2rem;
	margin: 1.6rem 0 0;
	padding-top: 1rem;
	border-top: 1px solid rgba(255, 255, 255, 0.12);
	font-size: 0.8rem;
	letter-spacing: 0.04em;
}

.site-footer a {
	color: rgba(244, 239, 230, 0.55);
	text-decoration: none;
}

.site-footer a:hover,
.site-footer a:focus-visible {
	color: var(--accent);
	text-decoration: underline;
}

/* --- Terms ---------------------------------------------------------------------------
 *
 * A draft until the dealer's own text arrives, and it says so at the top. The
 * notice is marked rather than styled like body text: a reader who skims has to
 * see that this is not finished before they rely on a figure in it.
 */
.front-terms-draft {
	padding: 0.7rem 0.9rem;
	border-radius: 10px;
	border: 1px solid rgba(242, 106, 27, 0.4);
	background: rgba(242, 106, 27, 0.08);
}

.front-terms-title {
	justify-self: start;
	margin: 1.4rem 0 0.2rem;
	color: var(--accent);
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.15rem;
	letter-spacing: 0.06em;
}


/* --- The brand filter ----------------------------------------------------------------
 *
 * Opened from VALMISTAJA on the JÄRJESTÄ row and drawn with the model filter's
 * button style, because it is the same kind of thing: a list of choices that
 * narrows the grid, each carrying the count it will produce.
 *
 * Its own row below the sort rows rather than in .front-head-right, where the
 * model filter lives: a category can have fifty brands, and fifty buttons in a
 * column capped at 760px would be a wall. Full width they wrap into a few lines.
 */
.front-brands {
	justify-self: stretch;
	width: 100%;
	min-width: 0;
}

.front-brands:empty {
	display: none;
}

.front-brands .front-model-row {
	display: flex;
	flex-wrap: wrap;
	gap: 0.35rem 0.5rem;
	margin: 0.2rem 0 0.6rem;
}

/* The second sort row. The first carries JÄRJESTÄ: and VALMISTAJA, this one the
   orderings - a row each, so a filter is not read as an ordering. */
.front-sort-row {
	justify-self: stretch;
	width: 100%;
	display: flex;
	justify-content: flex-end;
	margin: -0.3rem 0 0.2rem;
}


/* --- Legal pages ---------------------------------------------------------------------
 *
 * The terms, the privacy notice and the return policy. Long reference documents
 * that nobody reads front to back: somebody arrives wanting one section, so the
 * headings are at the top as links.
 *
 * The links are buttons, not anchors - the app routes on the address hash, and
 * an href="#section" would replace the route instead of scrolling. The global
 * button rule has to be undone for the same reason as everywhere else in this
 * file: without it each heading would be an orange pill.
 */
.legal-toc {
	justify-self: stretch;
	width: 100%;
	max-width: 46rem;
	margin: 0.2rem 0 1.4rem;
	padding: 0.9rem 1rem;
	display: flex;
	flex-direction: column;
	gap: 0.25rem;
	border-radius: 12px;
	border: 1px solid rgba(255, 255, 255, 0.14);
	background: rgba(255, 255, 255, 0.05);
}

.legal-toc-link {
	background: none;
	border: 0;
	border-radius: 0;
	padding: 0.1rem 0;
	width: auto;
	text-align: left;
	font: inherit;
	font-size: 0.92rem;
	letter-spacing: normal;
	text-transform: none;
	color: var(--accent-soft);
	cursor: pointer;
}

.legal-toc-link:hover,
.legal-toc-link:focus-visible {
	color: var(--accent);
	text-decoration: underline;
}

.legal-section {
	justify-self: stretch;
	width: 100%;
	max-width: 46rem;
	/* Room above so a heading scrolled to does not sit against the top edge of
	   the box, which reads as the page having been cut. */
	scroll-margin-top: 1.2rem;
}

.legal-list {
	margin: 0.2rem 0 0.8rem;
	padding-left: 1.2rem;
	max-width: 68ch;
	line-height: 1.55;
}

.legal-list li {
	margin: 0.15rem 0;
}

.legal-lines {
	margin: 0.4rem 0 0.9rem;
	line-height: 1.5;
}


/* --- The thank-you after a message ---------------------------------------------------
 *
 * In the middle of the screen, above everything, for a few seconds - the length
 * is in app.js, next to the timer that counts it. The message panel closes as
 * this appears, so it is the only thing on screen saying the message went,
 * which is why it is not a small note in a corner.
 *
 * The backdrop does not take clicks (pointer-events: none) so the shop stays
 * usable underneath; the box itself does, because clicking it closes it early.
 *
 * z-index above the chat widget's 25: it replaces the panel and must not appear
 * behind the bubble that used to open it.
 */
.chat-thanks {
	position: fixed;
	inset: 0;
	/* Above everything. The chat bubble is 25 and the profile 20; this replaces
	   the panel that was just closed, so it cannot appear behind the things that
	   sat over it. */
	z-index: 100;
	display: grid;
	/* align-content, not just place-items.
	 *
	 * The box is two elements - a title and a text - so the grid has two rows.
	 * With the default align-content: stretch those rows split the whole viewport
	 * between them, and place-items: center then centred each part inside its own
	 * half: the title floated in the middle of the top half and the text in the
	 * middle of the bottom, with the page showing between them. Packing the rows
	 * and centring them as a group is what makes the two read as one box. */
	align-content: center;
	justify-items: center;
	gap: 0;
	padding: 1.5rem;
	pointer-events: none;
}

.chat-thanks > * {
	pointer-events: auto;
}

.chat-thanks-title,
.chat-thanks-text {
	width: min(24rem, 100%);
	margin: 0;
	padding: 1.4rem 1.6rem 0;
	background: #14110f;
	color: #f4efe6;
	text-align: center;
	cursor: pointer;
}

.chat-thanks-title {
	padding-top: 1.6rem;
	border-radius: 16px 16px 0 0;
	border: 1px solid rgba(255, 255, 255, 0.16);
	border-bottom: 0;
	box-shadow: 0 18px 44px rgba(0, 0, 0, 0.5);
	font-family: "Bebas Neue", sans-serif;
	font-size: 1.5rem;
	letter-spacing: 0.06em;
	color: var(--accent);
}

.chat-thanks-text {
	padding: 0.5rem 1.6rem 1.6rem;
	border-radius: 0 0 16px 16px;
	border: 1px solid rgba(255, 255, 255, 0.16);
	border-top: 0;
	box-shadow: 0 18px 44px rgba(0, 0, 0, 0.5);
	font-size: 0.95rem;
	line-height: 1.45;
}

/* It appears rather than slides: the panel has just closed under it, and two
   movements at once read as the page jumping. */
@media (prefers-reduced-motion: no-preference) {
	.chat-thanks:not(.hidden) {
		animation: chat-thanks-in 0.18s ease-out;
	}
}

@keyframes chat-thanks-in {
	from { opacity: 0; }
	to { opacity: 1; }
}


/* The honeypot field: present in the markup, never on screen.
 *
 * Off-screen rather than display: none or visibility: hidden - some bots skip
 * fields that are not rendered at all, and the whole point is that this one
 * looks ordinary to them.
 *
 * It is out of the keyboard's reach and out of a screen reader's by its
 * attributes (tabindex, aria-hidden, autocomplete), not by this rule. That
 * matters: a trap a real person can fall into would silently destroy their
 * message, and the people most likely to fall in are the ones who cannot see
 * the screen. */
.chat-trap {
	position: absolute;
	left: -9999px;
	width: 1px;
	height: 1px;
	opacity: 0;
	pointer-events: none;
}

/* Päällekkäisyydet: the same part from both suppliers.
   ------------------------------------------------------------------ */

/* The old door on the catalogues page was a box with a label, a count and a
   paragraph of prose. It is three text links now (.admin-door), so its styles
   are gone with it - what is left of it is the class that greys a door with
   nothing behind it. */
.admin-duplicate-open-empty .admin-door-count {
	opacity: 0.55;
}

/* The page the door opens: one box, the whole width. */
.admin-grid-duplicates {
	grid-template-columns: minmax(0, 1fr);
}

/* The page's own title, at the size of one: the eyebrow it used to be sat at
   the size of a label on a box, and this is not a box on a page - it IS the
   page. */
.admin-dup-heading {
	margin: 0;
	color: var(--accent);
	font-size: 1.5rem;
	font-weight: 800;
	letter-spacing: 0.04em;
}

.admin-dup-head {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: 1rem;
	flex-wrap: wrap;
}

.admin-dup-head > div {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 0.25rem;
}

/* The search that makes the list, beside the heading that names it. Written as
   a text action rather than a button: it neither decides anything nor changes
   the shop, it only looks again - and a filled button beside the heading read
   as the page's main act, which it is not. */
/* At the other end of the tabs' row. Written as a text action rather than a
   button: it neither decides anything nor changes the shop, it only looks
   again, and a filled button up here read as the page's main act. */
.admin-panel .admin-dup-scan {
	margin-top: 0;
	margin-left: auto;
	padding-top: 0;
	padding-bottom: 0.3rem;
	white-space: nowrap;
}

.admin-panel .admin-dup-back {
	margin-top: 0;
	align-self: flex-start;
	color: rgba(255, 255, 255, 0.72);
}

/* The count sits in the eyebrow because it is the reason to look at the box at
   all - a panel titled PÄÄLLEKKÄISYYDET says nothing about whether there is
   work in it. */
.admin-dup-count {
	margin-left: 0.5rem;
	color: var(--accent);
	font-variant-numeric: tabular-nums;
	font-weight: 800;
}

.admin-dup-tabs {
	display: flex;
	flex-wrap: wrap;
	gap: 0.4rem 1.2rem;
	margin: 0.6rem 0 0.2rem;
}

/* The three lists under the title, at a size that reads as a heading of its
   own: they are what the page is divided into, not fine print. */
.admin-panel .admin-dup-tab {
	margin-top: 0;
	padding: 0 0 0.3rem;
	background: none;
	border: 0;
	border-radius: 0;
	color: rgba(255, 255, 255, 0.62);
	font: inherit;
	font-size: 0.95rem;
	font-weight: 700;
	letter-spacing: 0.06em;
	cursor: pointer;
}

/* Same trade-off as the admin menu: on this ground the underline carries the
   state, the hue only supports it. */
.admin-panel .admin-dup-tab-on {
	color: var(--accent);
	text-decoration: underline;
	text-decoration-thickness: 2px;
	text-underline-offset: 0.3em;
}

.admin-dup-list {
	list-style: none;
	margin: 0.6rem 0 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 0.9rem;
}

.admin-dup-item {
	padding: 0.75rem;
	border-radius: 10px;
	background: rgba(4, 10, 24, 0.55);
	border: 1px solid rgba(173, 193, 255, 0.24);
}

/* Why the scan thinks these two are the same part. Above the pair rather than
   below it: it is what the reader needs before comparing, not after. */
.admin-dup-reasons {
	margin: 0 0 0.6rem;
	font-size: 0.74rem;
	letter-spacing: 0.02em;
	color: rgba(255, 255, 255, 0.62);
}

/* One product on the left, its answers on the right: the same K&N filter was
   being asked about in four rows of a flat list, and the dealer's question is
   "this one - which of these is it". Stacked on a narrow screen, where side by
   side would be two squeezed columns. */
.admin-dup-group {
	display: grid;
	grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
	gap: 0.75rem;
	align-items: start;
}

@media (max-width: 62rem) {
	.admin-dup-group {
		grid-template-columns: minmax(0, 1fr);
	}
}

.admin-dup-candidates {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
	gap: 0.6rem;
	align-content: start;
}

/* One box, whether the product is the one being asked about or one of the
   answers: the same size and the same order inside, so the eye compares them
   instead of re-learning each one. Centred, because a box read against another
   box has no left edge of its own to hang on. */
.admin-dup-side,
.admin-dup-candidate {
	position: relative;
	display: flex;
	flex-direction: column;
	align-items: center;
	text-align: center;
	gap: 0.45rem;
	min-height: 21rem;
	padding: 0.9rem 0.8rem;
	border-radius: 8px;
	border: 1px solid rgba(173, 193, 255, 0.2);
	background: rgba(8, 16, 34, 0.5);
}

/* Why the scan thinks these two are the same part, at the top of the box and
   out from under the switch in the corner. */
.admin-dup-candidate .admin-dup-reasons {
	margin: 0;
	padding-right: 3.1rem;
	font-size: 0.7rem;
	text-align: left;
	width: 100%;
}

.admin-dup-side-chosen {
	border-color: var(--accent);
}

/* The hidden one is still readable - its price and stock are the point of
   keeping it - but it should not look like the one on sale. */
.admin-dup-side-hidden {
	opacity: 0.66;
}

.admin-dup-supplier {
	margin: 0;
	/* Room for the switch in the corner. */
	padding-right: 3.1rem;
	/* At the left edge rather than centred: the supplier and the state are what
	   the eye runs down from box to box, and a centred label shifts with the
	   length of the name. */
	width: 100%;
	text-align: left;
	font-size: 0.74rem;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: rgba(255, 255, 255, 0.72);
}

.admin-dup-tag {
	margin-left: 0.4rem;
	font-size: 0.68rem;
	font-weight: 700;
	letter-spacing: 0.04em;
	text-transform: none;
	color: rgba(255, 255, 255, 0.55);
}

/* In the shop and not in the shop, told apart by more than a word: the panel is
   scanned pair by pair, and the state has to register before the sentence is
   read. */
.admin-dup-tag-in {
	color: var(--accent);
}

.admin-dup-tag-out {
	color: rgba(255, 255, 255, 0.5);
	text-decoration: line-through;
	text-decoration-thickness: 1px;
}

.admin-dup-title {
	margin: 0;
	font-size: 0.95rem;
	font-weight: 700;
}

.admin-dup-choose {
	display: flex;
	flex-wrap: wrap;
	align-items: flex-end;
	gap: 0.5rem;
	margin-top: 0.45rem;
}

/* The eye, and the same words the button does: in the shop, or out of it. The
   shop's own tile carries the icon alone, because there the product is right
   under it; here the cards are read in pairs and the word is what tells the two
   apart at a glance. */
.admin-panel .admin-dup-hide {
	position: absolute;
	top: 0.5rem;
	right: 0.5rem;
	z-index: 2;
	margin-top: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 3.1rem;
	height: 3.1rem;
	padding: 0;
	border-radius: 50%;
}

.admin-dup-hide svg {
	width: 1.8rem;
	height: 1.8rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.7;
	stroke-linecap: round;
}

/* Already out of the shop: the button now says the way back in, and says it
   quietly - putting a product back is the rarer of the two. */
.admin-panel .admin-dup-hide-on {
	background: none;
	border: 1px solid rgba(173, 193, 255, 0.35);
	color: rgba(255, 255, 255, 0.8);
}

/* The one button that closes the question. It carries the orders' own classes,
   so the size and the look come from there; only the margin this row would add
   is taken off. */
.admin-panel .admin-dup-done {
	margin-top: 0;
	white-space: nowrap;
}

.admin-dup-actions {
	display: flex;
	align-items: center;
	justify-content: flex-end;
	gap: 1rem;
	margin-top: 0.6rem;
}

.admin-panel .admin-dup-actions button {
	margin-top: 0;
}

.admin-dup-empty {
	font-size: 0.85rem;
	color: rgba(255, 255, 255, 0.62);
}

/* The product's own title, as the way into it. A link rather than a button in
   looks: the row reads as a product, and the way in is the product's name. */
.admin-panel .admin-dup-link {
	margin-top: 0;
	padding: 0;
	background: none;
	border: 0;
	border-radius: 0;
	color: #fff;
	font: inherit;
	font-weight: 700;
	text-align: left;
	text-decoration: underline;
	text-decoration-color: rgba(255, 255, 255, 0.35);
	text-underline-offset: 0.25em;
	cursor: pointer;
}

.admin-panel .admin-dup-link:hover,
.admin-panel .admin-dup-link:focus-visible {
	background: none;
	color: var(--accent);
	text-decoration-color: var(--accent);
}

/* The product over the page.
   ------------------------------------------------------------------ */

/* Fixed rather than a second window: the list underneath keeps its scroll and
   its state, so closing this lands the reader back on the row they were on. */
.admin-peek {
	position: fixed;
	inset: 0;
	z-index: 90;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 1.25rem;
}

.admin-peek.hidden {
	display: none;
}

.admin-peek-backdrop {
	position: absolute;
	inset: 0;
	background: rgba(2, 6, 16, 0.72);
}

.admin-peek-card {
	position: relative;
	width: min(46rem, 100%);
	max-height: 86vh;
	overflow-y: auto;
	padding: 1.2rem 1.3rem 1.4rem;
	border-radius: 14px;
	border: 1px solid rgba(173, 193, 255, 0.28);
	background: #0a1224;
	box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}

.admin-panel .admin-peek-close,
.admin-peek-close {
	position: absolute;
	top: 0.5rem;
	right: 0.6rem;
	margin-top: 0;
	padding: 0 0.3rem;
	background: none;
	border: 0;
	color: rgba(255, 255, 255, 0.7);
	font-size: 1.6rem;
	line-height: 1;
	cursor: pointer;
}

.admin-peek-close:hover,
.admin-peek-close:focus-visible {
	color: var(--accent);
}

.admin-peek-title {
	margin: 0 2rem 0.8rem 0;
	/* Set rather than inherited: the card sits outside .admin-panel, whose
	   colour rules are what the rest of this view gets its white from. */
	color: #fff;
	font-size: 1.05rem;
	font-weight: 800;
}

/* The picture beside the figures, one under the other when there is no room:
   which of two filters this is shows in the photograph faster than in any row
   of text. */
.admin-peek-grid {
	display: grid;
	grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
	gap: 1rem;
	align-items: start;
}

@media (max-width: 34rem) {
	.admin-peek-grid {
		grid-template-columns: minmax(0, 1fr);
	}
}

.admin-peek-image {
	width: 100%;
	border-radius: 10px;
	background: #fff;
}

.admin-peek-text {
	margin: 0.9rem 0 0;
	font-size: 0.85rem;
	line-height: 1.5;
	color: rgba(255, 255, 255, 0.78);
}

/* The photograph beside the figures. Small: it is there to say "this is the
   round one and that is the teardrop", not to be looked at. */
/* A square tile whatever shape the photograph is. Without the fixed height a
   wide picture (some of the supplier's are banners) became a thin strip beside the
   figures, which says nothing about the part. */
.admin-panel .admin-dup-thumb {
	margin-top: 0;
	padding: 0.2rem;
	width: 6.5rem;
	height: 6.5rem;
	display: flex;
	align-items: center;
	justify-content: center;
	border: 0;
	border-radius: 8px;
	background: #fff;
	overflow: hidden;
	cursor: pointer;
	line-height: 0;
}

.admin-dup-thumb img {
	max-width: 100%;
	max-height: 100%;
	object-fit: contain;
	display: block;
}

/* No photograph: the frame stays, so the rows below it still line up across the
   card. A box shorter than its neighbour reads as a different kind of thing
   rather than as the same thing without a picture. */
.admin-dup-thumb-empty {
	background: rgba(255, 255, 255, 0.06);
	border: 1px dashed rgba(173, 193, 255, 0.28);
	cursor: default;
}

/* A state the dealer set, sitting on the row it belongs to rather than only in
   the name of a tab three lists away. */
.admin-dup-state {
	margin-right: auto;
	font-size: 0.72rem;
	font-weight: 800;
	letter-spacing: 0.06em;
	color: rgba(255, 255, 255, 0.62);
}

/* The state tags on each side. Capitals, because they are states and not
   sentences, and the eye picks them out of the row that way. */
.admin-dup-tag {
	letter-spacing: 0.05em;
}

/* The warning triangle. One mark in two places, so it is defined once: in the
   menu beside KATALOGIT, and in the corner of the box on the catalogues page.
   Orange rather than red - nothing is broken, something is waiting. */
.admin-nav-warn,
.admin-duplicate-warn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	color: var(--accent);
}

.admin-nav-warn.hidden,
.admin-duplicate-warn.hidden {
	display: none;
}

.admin-nav-warn svg,
.admin-duplicate-warn svg {
	width: 1.1rem;
	height: 1.1rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.8;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.admin-duplicate-warn svg circle {
	fill: currentColor;
	stroke: none;
}

.admin-nav-warn svg circle {
	fill: currentColor;
	stroke: none;
}

.admin-nav-warn {
	margin-left: 0.4rem;
	vertical-align: -0.2em;
}

/* Top right corner of the box, where a mark is looked for. */
.admin-duplicate-door {
	position: relative;
}

.admin-duplicate-warn {
	position: absolute;
	top: 0.85rem;
	right: 0.9rem;
}

.admin-duplicate-warn svg {
	width: 1.5rem;
	height: 1.5rem;
}

/* What is true about this product now: out of the shop, or gone from the
   supplier's range. Orange and upright rather than struck through - a line
   through a fact is this panel's way of saying it was undone, and these two
   are in force. */
.admin-product-state {
	color: var(--accent);
	font-weight: 700;
}

/* The hide switch on the product page. The same words and the same icon as the
   shop's tile carries; it is a button rather than a corner mark here because
   this page is read one product at a time and has room to say what it does. */
.admin-panel .admin-product-hide {
	margin-top: 0.8rem;
	display: inline-flex;
	align-items: center;
	gap: 0.45rem;
	padding: 0.5rem 1rem;
	font-size: 0.78rem;
}

.admin-product-hide svg {
	width: 1.1rem;
	height: 1.1rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.8;
	stroke-linecap: round;
}

/* Already out of the shop: the button now offers the way back, and says it
   quietly - putting a product back is the rarer of the two. */
.admin-panel .admin-product-hide-on {
	background: none;
	border: 1px solid rgba(173, 193, 255, 0.35);
	color: rgba(255, 255, 255, 0.85);
}

/* The box's own title, at the top of the box where every other box has it, and
   clear of the warning triangle in the corner. */
.admin-duplicate-door .eyebrow {
	margin-top: 0;
	padding-right: 2.4rem;
}

/* The wholesaler's name, top right, admin only (frontSupplierTag).
   Quiet: it is a note to the person minding the shop, not a label on the
   product, so it sits in the corner and does not compete with the price or with
   EI VARASTOSSA on the other side. */
.front-supplier-tag {
	position: absolute;
	top: 0.5rem;
	right: 0.5rem;
	z-index: 2;
	padding: 0.18rem 0.45rem;
	border-radius: 6px;
	background: rgba(4, 10, 24, 0.78);
	color: #fff;
	font-size: 0.62rem;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	white-space: nowrap;
	pointer-events: none;
}

.front-detail-image .front-supplier-tag {
	font-size: 0.7rem;
}

/* Counts on the three lists of the duplicates page. */
.admin-dup-tab-count {
	margin-left: 0.35rem;
	font-variant-numeric: tabular-nums;
	font-weight: 800;
	opacity: 0.85;
}

/* A product the dealer has taken out of the shop, drawn for the admin only.
   Greyed rather than removed: it is still there, it is just not for sale, and
   the whole point of hiding instead of deleting is that it can be seen. */
.front-product-hidden {
	position: relative;
}

.front-product-hidden .front-product-image,
.front-product-hidden .front-detail-image,
.front-product-hidden .front-product-body,
.front-product-hidden .front-detail-body {
	filter: grayscale(1);
	opacity: 0.55;
}

.front-product-hidden-tag {
	position: absolute;
	left: 0.5rem;
	bottom: 0.5rem;
	z-index: 2;
	padding: 0.18rem 0.45rem;
	border-radius: 6px;
	background: rgba(4, 10, 24, 0.82);
	color: #fff;
	font-size: 0.62rem;
	font-weight: 800;
	letter-spacing: 0.06em;
	pointer-events: none;
}

/* The switch itself, top left so it does not sit on the wholesaler's name.
   Above the greying (no filter of its own): a faded control on a faded tile
   would be hard to find at the moment it is wanted. */
.front-hide-btn {
	position: absolute;
	top: 0.5rem;
	left: 0.5rem;
	z-index: 3;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 1.9rem;
	height: 1.9rem;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: rgba(4, 10, 24, 0.78);
	color: #fff;
	cursor: pointer;
}

.front-hide-btn:hover,
.front-hide-btn:focus-visible {
	background: var(--accent);
}

.front-hide-btn-on {
	background: var(--accent);
}

/* Toimitus sovitaan erikseen.
 *
 * Silmän alapuolella samassa reunassa: ne ovat kaksi päätöstä samasta
 * tuotteesta ja voivat olla päällä yhtä aikaa, joten ne eivät voi jakaa
 * paikkaa. Alla eikä vieressä, koska kortin yläreuna on kapea ja kaksi
 * nappia vierekkäin peittäisi kuvasta sen kohdan jota katsotaan. */
.front-freight-btn {
	position: absolute;
	top: 2.7rem;
	left: 0.5rem;
	z-index: 3;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 1.9rem;
	height: 1.9rem;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: rgba(4, 10, 24, 0.78);
	color: #fff;
	cursor: pointer;
}

.front-freight-btn:hover,
.front-freight-btn:focus-visible {
	background: var(--accent);
}

.front-freight-btn-on {
	background: var(--accent);
}

.front-freight-btn svg {
	width: 1.05rem;
	height: 1.05rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.8;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.front-hide-btn svg {
	width: 1.05rem;
	height: 1.05rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.8;
	stroke-linecap: round;
}

.front-product,
.front-detail {
	position: relative;
}

/* The fact rows inside a duplicate card. The panel's own label column is 10.5rem
   wide, which is most of the anchor's 20rem: the values wrapped onto three
   lines each and the card read as a column of fragments. */
.admin-dup-side .admin-catalogue-row,
.admin-dup-candidate .admin-catalogue-row {
	grid-template-columns: 7rem minmax(0, 1fr);
	gap: 0.4rem;
}

/* The facts keep their two columns and their left edge - a label above a value
   in the middle of a box is a list nobody can scan - but the block itself sits
   in the centre like everything else, and at the bottom so the boxes line up
   whatever the title's length. */
.admin-dup-side .admin-catalogue-meta,
.admin-dup-candidate .admin-catalogue-meta {
	font-size: 0.78rem;
	gap: 0.22rem;
	text-align: left;
	width: 100%;
	max-width: 14rem;
	margin-top: auto;
}

/* The number beside a section's own heading: how many of the thing this page
   is about. Set apart from the word rather than glued to it, and in the same
   quiet grey the other counts use. */
.admin-heading-count {
	margin-left: 0.6rem;
	font-family: "Space Grotesk", sans-serif;
	font-size: 0.85rem;
	font-variant-numeric: tabular-nums;
	letter-spacing: 0;
	color: rgba(255, 255, 255, 0.6);
}

/* --- Where an admin page is, and the way out of it ------------------------ */

/* The same shape and the same face the shop uses over its own lists
   (.front-breadcrumb): a page inside the back office is still a page, and a
   back button above a heading said the same thing twice. */
.admin-crumbs {
	grid-column: 1 / -1;
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.5rem;
	margin: 0 0 0.4rem;
	font-family: "Bebas Neue", sans-serif;
	letter-spacing: 0.03em;
	font-size: clamp(1rem, 2.2vw, 1.4rem);
}

.admin-panel .admin-crumb {
	margin: 0;
	padding: 0;
	border: 0;
	background: none;
	font: inherit;
	letter-spacing: inherit;
	color: rgba(244, 239, 230, 0.85);
	cursor: pointer;
}

.admin-panel .admin-crumb:hover,
.admin-panel .admin-crumb:focus-visible {
	color: var(--accent);
	background: none;
}

/* The step the reader is on: not a link, and the loud one. */
.admin-panel .admin-crumb-now {
	color: #fff;
	cursor: default;
}

.admin-crumb-sep {
	color: rgba(244, 239, 230, 0.45);
}

/* --- The catalogues page's three doors ------------------------------------ */

/* Three lines, each a count and a way in. The explanations live behind them,
   on the pages they explain: a box of prose beside the catalogues was read
   once and scrolled past for ever after.

   Close together on purpose: they are one list of three, and the air between
   them made them read as three separate boxes with nothing in them. */
.admin-door-list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	gap: 0.75rem;
}

.admin-door-list li {
	display: flex;
	align-items: center;
	gap: 0.5rem;
}

.admin-door {
	display: flex;
	align-items: baseline;
	gap: 0.6rem;
	/* No margin of its own: the panel gives its buttons a 16 px top margin, and
	   in a list of three that margin WAS the spacing - it pushed each line down
	   inside its own row, so the warning triangle beside it sat eight pixels
	   high. The gap between the rows is the spacing now. */
}

/* .admin-panel button gives every button a top margin, and in a list of three
   that margin WAS the spacing: it pushed each line down inside its own row, so
   the warning triangle beside it sat eight pixels high. The gap between the
   rows is the spacing. */
.admin-panel .admin-door {
	margin: 0;
	padding: 0;
}

/* Every door's count is the size of the words beside it.
 *
 * The duplicates used to be set larger, on the reasoning that work waiting to
 * be decided should be the loud half of its line. Two counts in two sizes on
 * one list of three doors read as a layout slip rather than as a priority, so
 * they are one size now. Tabular figures and the panel's own ink, because the
 * shop's ink colour is invisible on this dark ground. */
.admin-door-count {
	font-variant-numeric: tabular-nums;
	color: #f4efe6;
}

/* Something is waiting for a decision, and nothing else. On the same baseline
   as the line it belongs to: floated above it, it read as a mark on the box. */
.admin-door-warn {
	display: inline-flex;
	align-items: center;
	align-self: center;
}

.admin-door-warn svg {
	width: 1.05rem;
	height: 1.05rem;
	display: block;
	fill: none;
	stroke: var(--accent);
	stroke-width: 1.8;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.admin-door-warn circle {
	fill: var(--accent);
	stroke: none;
}

/* --- What a price is made of --------------------------------------------- */

.admin-pricing {
	margin-top: 1rem;
	padding-top: 0.55rem;
	border-top: 1px solid rgba(255, 255, 255, 0.14);
}

/* The heading sits against its own rule: the rule is what separates the
   pricing from the rows above it, and a heading floating in the middle of that
   gap belonged to neither side. */
.admin-panel .admin-pricing .eyebrow {
	margin-top: 0;
}

.admin-pricing-pick {
	margin-top: 0.6rem;
	display: flex;
	align-items: center;
	gap: 0.6rem;
	font-size: 0.9rem;
}

/* Flat, like the shop's own year picker.
 *
 * The browser's native select is a raised, shaded control from another decade -
 * beside the shop's own flat one it reads as a piece of a different program.
 * Same construction as .front-year-select: no appearance, a thin border, and
 * the arrow drawn with two gradients. */
.admin-pricing-pick select {
	appearance: none;
	field-sizing: content;
	min-width: 5.5rem;
	font: inherit;
	padding: 0.3rem 2.1rem 0.3rem 0.7rem;
	border: 1px solid rgba(244, 239, 230, 0.35);
	border-radius: 4px;
	background-color: transparent;
	background-image:
		linear-gradient(45deg, transparent 50%, currentColor 50%),
		linear-gradient(135deg, currentColor 50%, transparent 50%);
	background-position: right 1.05rem center, right 0.75rem center;
	background-size: 0.32rem 0.32rem, 0.32rem 0.32rem;
	background-repeat: no-repeat;
	color: inherit;
	cursor: pointer;
}

.admin-pricing-pick select:hover {
	border-color: var(--accent);
	color: var(--accent);
}

/* The open list is the operating system's and cannot be styled; the options can
   be told what ground they are on, so the panel's dark one does not leave dark
   text on dark. */
.admin-pricing-pick option {
	background: #17150f;
	color: #f4efe6;
}

/* "Tallennettu", for a moment. Quiet and beside the choice rather than in the
   page's status line: it is about this one dropdown. */
.admin-pricing-saved {
	font-size: 0.8rem;
	color: var(--accent-soft);
}

/* --- What has been done to the catalogue by hand -------------------------- */

/* One row per product: the picture and the story across the top, HISTORIA in
   the bottom right corner, and the story it opens underneath - full width,
   where a list of events reads as a list rather than as a column squeezed
   beside the product it belongs to. */
.admin-edited-row {
	position: relative;
	display: grid;
	gap: 0.2rem;
	/* The line closes the product, and everything belonging to it is above the
	   line: HISTORIA and the story it opens were drawn under the button's own
	   border and read as the next product's. The padding is the air between the
	   last thing said about this product and the line that ends it - and at the
	   sides, the air between the picture and the box's own edge, which the
	   picture was pressed against. */
	padding: 0.2rem 0.6rem 0.7rem;
	border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

/* HISTORIA in the bottom right corner, the same distance from the edge as the
   eye above it: two controls in the same corner at two different insets read as
   one of them having slipped. */
.admin-panel .admin-edited-row .admin-edited-history {
	/* No margin of its own: the row's own padding is the distance from the
	   edge, the same one the eye above it keeps. */
	margin: 0;
	justify-self: end;
}

/* The eye in the corner, over the product's own line: one press puts a hidden
   product back on sale without opening it. The same symbol and the same round
   button the shop's tiles carry. */
/* .admin-panel in the selector: the panel gives every button a 16 px top
   margin, and on an absolutely placed icon that margin is 16 pixels of drift
   from the corner it was told to sit in. */
.admin-panel .admin-edited-restore {
	position: absolute;
	/* The row's own padding, top and side, so the eye sits in the corner of the
	   same box the row's content sits in.
	 *
	 * It used to be inset 0.6rem from the top as well, which works on a tall
	 * row and fails on a short one: a 27 px eye inset 10 px into a 63 px row is
	 * two thirds of the way to the middle, and it read as an icon floating in
	 * the row rather than one placed in its corner. The top inset follows the
	 * row's top padding now, so the corner is a corner at any height. */
	top: 0.2rem;
	right: 0.6rem;
	z-index: 2;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 1.7rem;
	height: 1.7rem;
	margin: 0;
	/* Not the admin button scale: this is a round icon in a corner, the same
	   shape the shop's tiles carry, and the standard sizes a button with words
	   in it. See docs/ulkoasun-mitat.md. */
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: rgba(4, 10, 24, 0.85);
	color: #fff;
	cursor: pointer;
}

.admin-edited-restore:hover,
.admin-edited-restore:focus-visible {
	background: var(--accent);
}

.admin-edited-restore svg {
	width: 1rem;
	height: 1rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.8;
	stroke-linecap: round;
	stroke-linejoin: round;
}

/* A hidden product carries no mark of its own down the side. The word
   PIILOTETTU on the row and the eye in the corner say it twice already, and a
   third mark for one fact is one more thing to look at. */

.admin-hidden-restore {
	flex: 0 0 auto;
	align-self: center;
	white-space: nowrap;
}

.admin-hidden-empty,
.admin-edited-empty {
	color: rgba(255, 255, 255, 0.6);
}

/* When and by whom, after the change itself. */
/* The row's own last line: what happened most recently. The rest is behind
   HISTORIA, because a product edited five times filled the row with five lines
   and pushed the next product off the screen. */
.admin-edited-latest {
	font-size: 0.85rem;
	color: #fff;
}

/* The story, set in from the row's own edge so its rule reads as a rule rather
   than as the box's border: flush left it was in line with the picture above it
   and disappeared into the edge. */
.admin-edited-events {
	list-style: none;
	margin: 0.35rem 0 0 3.6rem;
	padding: 0 0 0 0.7rem;
	border-left: 2px solid rgba(255, 255, 255, 0.28);
	display: grid;
	gap: 0.25rem;
	font-size: 0.85rem;
}

.admin-panel .admin-edited-history {
	white-space: nowrap;
}

.admin-edited-when {
	color: rgba(255, 255, 255, 0.5);
	font-size: 0.78rem;
}

.admin-edited-supplier {
	margin-left: 0.5rem;
	font-size: 0.7rem;
	letter-spacing: 0.04em;
	color: rgba(255, 255, 255, 0.55);
}

/* Mitä ajossa olevalta kaupalta puuttuu.
 *
 * Varoituksen väri eikä korostusväri: korostus tarkoittaa tässä paneelissa
 * "tässä on jotain tehtävää", ja tämä tarkoittaa "tämä on rikki". Ne eivät saa
 * näyttää samalta. */
.admin-health {
	margin: 0 0 1.2rem;
	padding: 0.9rem 1.1rem;
	border-left: 3px solid #b3261e;
	border-radius: 0 6px 6px 0;
	background: rgba(179, 38, 30, 0.12);
}

.admin-health-title {
	margin: 0 0 0.4rem;
	font-family: "Bebas Neue", sans-serif;
	font-size: 1rem;
	letter-spacing: 0.06em;
	color: #ff6b5e;
}

.admin-health-list {
	margin: 0;
	padding-left: 1.1rem;
	font-size: 0.86rem;
	line-height: 1.5;
}

/* --- A hiding that was done as a list ------------------------------------- */

/* One decision, one row, above the single products it would otherwise bury.
   The row is a link to what is in the list and carries the press that puts the
   whole thing back. */
.admin-batch-row {
	position: relative;
	/* The same border and the same side padding as a product's row, so the two
	   kinds of row sit in one column rather than in two lists. */
	padding: 0.2rem 0.6rem 0.7rem;
	border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.admin-batch-row:hover {
	background: rgba(255, 255, 255, 0.06);
}

.admin-panel .admin-batch-open {
	display: flex;
	align-items: center;
	gap: 0.7rem;
	width: 100%;
	margin: 0;
	padding: 0.5rem 2.4rem 0.2rem 0;
	border: 0;
	background: none;
	color: inherit;
	text-align: left;
	/* Not the admin button scale: this is a whole row that happens to be
	   pressable, like the product rows above and below it, and the standard
	   sizes a button with words in a box. The right-hand 2.4rem is room for
	   the eye in the corner, so a long title never runs under it.
	   See docs/ulkoasun-mitat.md. */
	cursor: pointer;
}

/* The number is what the row is read for, so it is the size of a heading and
   the only thing in the accent colour. "118" answers the question before the
   words underneath it are read at all. */
.admin-batch-count {
	flex: none;
	min-width: 2.6rem;
	font-size: 1.6rem;
	font-weight: 700;
	line-height: 1;
	color: var(--accent);
	font-variant-numeric: tabular-nums;
}

.admin-batch-facts {
	display: grid;
	gap: 0.15rem;
	min-width: 0;
}

.admin-batch-title {
	font-size: 0.86rem;
	line-height: 1.3;
}

.admin-batch-when {
	font-size: 0.78rem;
	color: rgba(255, 255, 255, 0.5);
}

/* The same round eye in the same corner as a product's own, because it does
   the same thing one level up. Same reason for the .admin-panel prefix: the
   panel's 16 px button margin would push it out of the corner. */
.admin-panel .admin-batch-row .admin-edited-restore {
	top: 0.2rem;
	right: 0.6rem;
}

/* --- What has been edited by hand ---------------------------------------- */

.admin-edited-image {
	width: 44px;
	height: 44px;
	object-fit: contain;
	border-radius: 6px;
	background: #fff;
	flex: 0 0 auto;
}

.admin-edited-image-empty {
	border: 1px dashed rgba(255, 255, 255, 0.25);
}

.admin-edited-facts {
	display: grid;
	gap: 0.15rem;
	min-width: 0;
	/* Room for the eye in the corner, so a long title does not run under it. */
	padding-right: 2.2rem;
}

/* The supplier's value, an arrow, and what the shop says now. */
.admin-edited-change {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.4rem;
	font-size: 0.85rem;
}

.admin-edited-field {
	color: rgba(255, 255, 255, 0.6);
}

.admin-edited-from {
	text-decoration: line-through;
	color: rgba(255, 255, 255, 0.55);
}

.admin-edited-to {
	font-weight: 600;
}

.admin-edited-hidden {
	margin-left: 0.5rem;
	font-size: 0.7rem;
	letter-spacing: 0.06em;
	color: var(--accent);
}

/* Aukioloajat yllapidossa: paiva ja aika rinnakkain, sama leveys kuin muilla
   lomakkeilla. Paiva on kapeampi kuin aika, koska "Lauantai" on lyhyempi kuin
   "Sopimuksen mukaan" - ja juuri sita pidempaa tekstia kenttaan kirjoitetaan. */
.admin-hours-rows {
	display: grid;
	gap: 0.5rem;
	margin-top: 0.8rem;
}

.admin-hours-row {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
	gap: 0.5rem;
}

/* Osoiteryhmän otsikko kassalla. Pieni ja väljä: se on väliotsikko kenttien
   seassa eikä kilpaile TILAAJA-otsikon kanssa. */
.checkout-address-title {
	margin: 0.4rem 0 -0.2rem;
	display: flex;
	flex-direction: column;
	gap: 0.15rem;
}

.checkout-group-title {
	font-size: 0.78rem;
	font-weight: 700;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	opacity: 0.75;
}

/* Selite näkyy vain noudossa: se kertoo miksi osoitetta kysytään silloin kun
   paketti ei mene minnekään. */
.checkout-group-note {
	font-size: 0.78rem;
	opacity: 0.6;
}

/* "(valinnainen)" kentän nimen perässä. Pienempi ja haaleampi kuin nimi itse:
   se on lisätieto eikä osa kentän nimeä, ja kassalla pitää erottua kumpi on
   kumpi silmäyksellä. */
/* "(valinnainen)" kentän nimen perässä, samalla rivillä kun tilaa on.
   Haaleampi kuin nimi itse: se on lisätieto eikä osa kentän nimeä. */
.checkout-field-optional {
	display: inline;
	margin: 0;
	font-weight: 400;
	text-transform: none;
	letter-spacing: 0;
	opacity: 0.6;
}

/* Laskutusosoitteen ruksi: oma rivinsä kenttien jatkoksi, ei kentän näköinen.
   Se on kysymys eikä kenttä, ja sen vastaus avaa neljä uutta kenttää.
   .checkout-field span on display: block, joka pudotti tekstin ruksin alle -
   siksi molemmat asetetaan tässä takaisin riviin. */
.checkout-billing-toggle {
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: 0.55rem;
	margin-top: 0.7rem;
}

.checkout-billing-toggle input[type="checkbox"] {
	width: 1.05rem;
	height: 1.05rem;
	flex: 0 0 auto;
	margin: 0;
	padding: 0;
}

.checkout-billing-toggle span {
	display: inline;
	margin: 0;
	font-size: 0.92rem;
	opacity: 0.9;
}

/* Maavalitsin on kentän näköinen, ei selaimen oman näköinen.
 *
 * Samat mitat ja sama reuna kuin .checkout-field input -säännössä: samalla
 * rivillä olevien kenttien on oltava saman korkuisia, tai lomake näyttää
 * siltä kuin yksi niistä olisi eri lomakkeesta. appearance: none ottaa pois
 * selaimen oman kohokuvioinnin, ja nuoli piirretään itse litteänä. */
.checkout-select {
	appearance: none;
	-webkit-appearance: none;
	width: 100%;
	box-sizing: border-box;
	height: 2.5rem;
	padding: 0 2rem 0 0.7rem;
	border: 1px solid rgba(255, 255, 255, 0.3);
	border-radius: 10px;
	background-color: rgba(255, 255, 255, 0.95);
	color: var(--ink);
	font: inherit;
	font-size: 0.95rem;
	/* Nuoli taustakuvana: kaksi viivaa, sama muste kuin tekstillä. SVG datana,
	   koska ulkoinen tiedosto olisi yksi pyyntö lisää yhdestä nuolesta. */
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%23111' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
	background-repeat: no-repeat;
	background-position: right 0.75rem center;
	background-size: 0.7rem;
}

.checkout-select:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 1px;
}
