/* Base layer — fonts, resets, global element defaults. Sections get their
   own styles in Stage 2. */

@font-face {
    font-family: 'Poppins';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../fonts/poppins-400.woff2') format('woff2');
}

@font-face {
    font-family: 'Poppins';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../fonts/poppins-500.woff2') format('woff2');
}

@font-face {
    font-family: 'Poppins';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../fonts/poppins-600.woff2') format('woff2');
}

*,
*::before,
*::after {
    box-sizing: border-box;
}

/* `hidden` is state, not styling, so it must beat any component's `display`.
   The UA sheet's `[hidden]{display:none}` loses to a single class selector,
   which is how the quote form's Back and Submit buttons stayed painted on
   every step even while JS had them hidden (found in 4.2 verification).
   Components must never need to repeat this rule for themselves. */
[hidden] {
    display: none !important;
}

body {
    margin: 0;
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: 1.6;
    color: var(--secondary);
    background: var(--white);
    -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
h4 {
    color: var(--primary);
    font-weight: 600;
    line-height: 1.2;
    margin: 0 0 var(--space-16);
    /* Finish-quality checklist (7.1, §5): no orphan word on its own last
       line. Unsupported browsers just ignore the property — no fallback
       needed. */
    text-wrap: balance;
}

h1 { font-size: var(--text-h1); }
h2 { font-size: var(--text-h2); }
h3 { font-size: var(--text-h3); }
h4 { font-size: var(--text-h4); }

p {
    margin: 0 0 var(--space-16);
}

a {
    color: var(--primary);
    transition: color var(--motion-micro);
}

a:hover {
    color: var(--green-dark);
}

img {
    max-width: 100%;
    height: auto;
}

:focus-visible {
    outline: 2px solid var(--green);
    outline-offset: 2px;
}

/* Finish-quality checklist (7.1, 02-design-system.md §5): the browser's own
   text-selection blue never matches the brand. */
::selection {
    background: var(--green-light);
    color: var(--primary);
}

.u-container {
    max-width: var(--container-max);
    margin-inline: auto;
    padding-inline: var(--space-24);
}

.eyebrow {
    text-transform: uppercase;
    letter-spacing: var(--eyebrow-tracking);
    color: var(--green-dark);
    font-size: var(--text-sm);
    font-weight: 600;
    margin: 0 0 var(--space-16);
}

/* Buttons — pill shape is the signature shape (02-design-system.md §2).
   Hover: dark bg / green border (§3 micro-interactions). */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-8);
    border: 1px solid transparent;
    border-radius: var(--radius-pill);
    padding: var(--space-16) var(--space-24);
    font-family: var(--font-sans);
    font-size: var(--text-button);
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    transition: background-color var(--motion-micro), border-color var(--motion-micro), color var(--motion-micro), box-shadow var(--motion-micro), transform var(--motion-micro);
}

/* Finish-quality checklist (7.1, §5): every ajax-form.js / quote-form.js
   submit disables its button while sending — without this, that state was
   the plain browser default (a faded native grey) on every .btn variant
   except the one quote/login button that already had its own rule. */
.btn[disabled] {
    opacity: 0.7;
    cursor: progress;
}

/* Loading spinner (7.1, §5 — the mockup's Figma "Spiner" component,
   06-quote-form-ux.md §96: "disabled button + spinner during send"). Keyed
   off [data-label-sending] so it only appears on the four real submit
   buttons (quote, contact, callback, login) that already swap their label
   for "Sender…" — no markup change needed in any of those templates.
   currentColor means one rule serves every .btn variant's own text color. */
.btn[disabled][data-label-sending]::before {
    content: '';
    width: 16px;
    height: 16px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: btn-spin 0.6s linear infinite;
}

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

@media (prefers-reduced-motion: reduce) {
    .btn[disabled][data-label-sending]::before {
        /* A frozen half-ring reads as a rendering glitch, not "no motion" —
           the swapped "Sender…" text alone still carries the loading state. */
        display: none;
    }
}

.btn--light {
    background: var(--white);
    color: var(--primary);
    border-color: var(--neutral-800);
}

.btn--light:hover {
    background: var(--bg-dark);
    border-color: var(--green);
    color: var(--white);
}

/* Chunky "keycap" pill CTA — export CSS: 4px offset shadow, hover presses
   the button down into its shadow (shadow removed + translateY(4px)). */
.btn--primary {
    height: 56px;
    background: var(--green);
    color: var(--white);
    box-shadow: 0 4px 0 0 var(--green-dark);
}

.btn--primary:hover {
    background: var(--green-dark);
    box-shadow: none;
    transform: translateY(4px);
    color: var(--white);
}

.btn--secondary {
    height: 56px;
    background: var(--neutral-200);
    color: var(--secondary);
    border-color: var(--neutral-800);
    box-shadow: 0 4px 0 0 var(--neutral-400);
}

.btn--secondary:hover {
    background: var(--neutral-400);
    box-shadow: none;
    transform: translateY(4px);
    color: var(--secondary);
}

@media (max-width: 767px) {
    .btn--primary,
    .btn--secondary {
        height: 48px;
    }
}

/* CTA on dark section backgrounds (the quote section, #forespoergsel).
   Same keycap shape as --primary/--secondary so the dark section's button
   reads as part of the same family (owner note 2026-07-18); export values:
   bg-dark fill, translucent neutral-800 border and shadow. */
.btn--dark {
    height: 56px;
    background: var(--bg-dark);
    color: var(--white);
    border-color: rgb(209 223 229 / .4);
    font-weight: 600;
    box-shadow: 0 4px 0 0 rgb(209 223 229 / .4);
}

.btn--dark:hover {
    background: var(--primary);
    box-shadow: none;
    transform: translateY(4px);
    color: var(--white);
}

@media (max-width: 767px) {
    .btn--dark {
        height: 48px;
    }
}

/* Form controls — one definition for every form on the site (quote,
   contact, callback, login). Export values: 56px tall, white fill, 8px
   radius, 2px border that is invisible at rest and turns green on focus,
   so focus never shifts layout. */
.field {
    display: flex;
    flex-direction: column;
    gap: var(--space-8);
}

.field > span,
.field > label {
    color: var(--primary);
    font-size: var(--text-sm);
    font-weight: 500;
}

.input {
    width: 100%;
    height: 56px;
    padding-inline: var(--space-16);
    border: 2px solid var(--white);
    border-radius: var(--radius-sm);
    background: var(--white);
    color: var(--primary);
    font-family: var(--font-sans);
    font-size: var(--text-input);
    transition: border-color var(--motion-micro);
}

textarea.input {
    height: auto;
    min-height: 160px;
    padding-block: var(--space-16);
    resize: vertical;
}

.input::placeholder {
    color: var(--grey);
}

.input:focus {
    outline: none;
    border-color: var(--green);
}

.input:focus-visible {
    outline: none;
}

/* On white backgrounds the invisible white border would leave the field
   edgeless — give it the neutral outline instead. */
.input--outlined {
    border-color: var(--neutral-800);
}

/* Validation (4.2). The message sits under its field and is only ever filled
   by JS or, from 4.3, by the server — an empty slot stays `hidden`.
   Colour is never the only signal: the message itself carries the meaning
   (WCAG 1.4.1), and aria-invalid carries it to assistive tech. */
.field-error {
    color: var(--error);
    font-size: var(--text-sm);
    line-height: 1.35;
}

.input[aria-invalid="true"],
.input[aria-invalid="true"]:focus {
    border-color: var(--error);
}

/* Choice groups mark themselves rather than each option inside them. */
[data-field].is-invalid {
    border-radius: var(--radius-sm);
    box-shadow: 0 0 0 2px var(--error);
}

@media (prefers-reduced-motion: reduce) {
    .btn--dark,
    .input {
        transition: none;
    }
}

.login-page {
    max-width: 400px;
    padding-block: var(--section-pad);
}

.login-page form {
    display: flex;
    flex-direction: column;
    gap: var(--space-16);
}

.login-page label {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    font-size: var(--text-sm);
    color: var(--primary);
}

.login-page input[type="email"],
.login-page input[type="password"] {
    border: 1px solid var(--neutral-800);
    border-radius: var(--radius-sm);
    padding: var(--space-16);
    font-size: var(--text-input);
    font-family: var(--font-sans);
}

.login-page__remember {
    flex-direction: row !important;
    align-items: center;
    gap: var(--space-8) !important;
}

/* Form-level error (5.10) — the same red as a field message, but it belongs
   to the form, so it drops the paragraph margin the flex gap already covers. */
.login-page__error {
    margin: 0;
}

/* Service/niche detail pages — readable CMS-authored prose (service.php,
   niche.php). Narrower than the site container for line-length. */
.service-detail {
    max-width: 760px;
    padding-block: var(--section-pad);
}

/* CMS body headings are deliberately smaller than their structural level:
   these are section headings inside an article, not page-level titles, and
   the h1 above them already carries that weight.

   The selector followed the content from h3 to h2 in 7.5 (the heading-level
   shift that fixed the h1→h3 skip). Without moving it, the rule matched
   nothing and every service/town section heading silently fell back to the
   global `h2` size — 24–32px instead of 20px, and no top margin. The size
   here is the *visual* decision and it hasn't changed; only the tag it
   attaches to did. */
.service-detail__body h2 {
    font-size: var(--text-h4);
    margin-top: var(--space-32);
}

/* The second level the editor can still reach (spec §5: headings 2–3) —
   one step down so the two read as a hierarchy rather than as equals. */
.service-detail__body h3 {
    font-size: var(--text-lg);
    margin-top: var(--space-24);
}

.service-detail__body ul {
    margin: 0 0 var(--space-16);
    padding-left: var(--space-24);
}

.service-detail__body li {
    margin-bottom: var(--space-8);
}

.service-detail__cta {
    margin-top: var(--space-32);
}

/* Town pages (6.3) — the service links every town page carries, so each one
   interlinks with the services it covers (04-seo-strategy.md §3). */
.town-services {
    margin-top: var(--space-40);
    padding-top: var(--space-32);
    border-top: 1px solid var(--neutral-800);
}

.town-services__title {
    margin: 0 0 var(--space-16);
    font-size: var(--text-h4);
}

.town-services__list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-8) var(--space-16);
    margin: 0;
    padding: 0;
    list-style: none;
}

.town-services__list a {
    display: inline-block;
    padding: var(--space-8) var(--space-16);
    border: 1px solid var(--neutral-800);
    border-radius: var(--radius-pill);
    color: var(--primary);
    font-size: var(--text-sm);
    text-decoration: none;
    transition: border-color var(--motion-micro), color var(--motion-micro);
}

.town-services__list a:hover {
    border-color: var(--green);
    color: var(--green-dark);
}

@media (prefers-reduced-motion: reduce) {
    .town-services__list a {
        transition: none;
    }
}

/* Sticky mobile CTA (js/sticky-cta.js). Ships with the `hidden` attribute
   and is only ever revealed by JS, so it can never cover content when the
   script does not run. */
.sticky-cta {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 40;
    padding: var(--space-16) var(--space-24) calc(var(--space-16) + env(safe-area-inset-bottom));
    background: var(--white-transparent);
    -webkit-backdrop-filter: blur(16px);
    backdrop-filter: blur(16px);
    border-top: 1px solid var(--neutral-800);
    transform: translateY(100%);
    transition: transform var(--motion-step) var(--ease-out);
}

.sticky-cta.is-visible {
    transform: none;
}

.sticky-cta .btn {
    width: 100%;
}

@media (min-width: 992px) {
    .sticky-cta {
        display: none;
    }
}

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

/* Overlay navigation (js/overlay-router.js). The element is built by JS, so
   none of this applies until the router is running — with JS off the links
   are plain navigations and no overlay exists. */
.overlay {
    position: fixed;
    inset: 0;
    z-index: 100;
}

.overlay__backdrop {
    position: absolute;
    inset: 0;
    background: rgb(36 57 77 / .5);
    opacity: 0;
    transition: opacity var(--motion-step) var(--ease-out);
}

.overlay.is-open .overlay__backdrop {
    opacity: 1;
}

.overlay__dialog {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    max-width: 900px;
    margin-inline: auto;
    background: var(--white);
    box-shadow: 0 0 40px rgb(36 57 77 / .18);
    opacity: 0;
    transform: translateY(24px);
    transition: opacity var(--motion-step) var(--ease-out), transform var(--motion-step) var(--ease-out);
}

.overlay.is-open .overlay__dialog {
    opacity: 1;
    transform: none;
}

@media (min-width: 768px) {
    .overlay__dialog {
        /* Inset top/bottom only — left/right must stay 0 so the max-width
           plus `margin-inline: auto` is what centres the dialog. */
        inset: var(--space-40) 0;
        border-radius: var(--radius-lg);
    }
}

.overlay__close {
    position: sticky;
    top: 0;
    align-self: flex-end;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    margin: var(--space-16) var(--space-16) 0;
    border: 1px solid var(--neutral-800);
    border-radius: var(--radius-pill);
    background: var(--white);
    color: var(--primary);
    cursor: pointer;
    transition: background-color var(--motion-micro);
}

.overlay__close:hover {
    background: var(--neutral-200);
}

.overlay__panel {
    flex: 1;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Finish-quality checklist (7.1, §5): a thin brand-toned thumb instead of
       the browser default — this is the one scrollable surface the mockup
       calls out by name, since it's the only place a scrollbar sits inside
       a rounded white panel rather than at the browser's own edge. */
    scrollbar-width: thin;
    scrollbar-color: var(--neutral-800) transparent;
}

.overlay__panel::-webkit-scrollbar {
    width: 8px;
}

.overlay__panel::-webkit-scrollbar-track {
    background: transparent;
}

.overlay__panel::-webkit-scrollbar-thumb {
    background: var(--neutral-800);
    border-radius: var(--radius-pill);
}

.overlay__panel::-webkit-scrollbar-thumb:hover {
    background: var(--green);
}

/* The fetched page ships its own container paddings; drop the top one so the
   content starts under the close button instead of a fixed-header gap. */
.overlay__panel .service-detail {
    padding-top: 0;
}

@media (prefers-reduced-motion: reduce) {
    .overlay__backdrop,
    .overlay__dialog {
        transition: none;
    }
}

/* Scroll reveals (js/reveal.js). Everything below is gated on
   .is-reveal-ready, which only JS ever sets — with JS off, or under
   reduced motion, content simply renders normally and is never hidden. */
.is-reveal-ready [data-reveal].is-reveal-pending > * {
    opacity: 0;
    transform: translateY(20px);
}

.is-reveal-ready [data-reveal].is-revealed > * {
    opacity: 1;
    transform: none;
    transition:
        opacity var(--motion-reveal) var(--ease-out) var(--reveal-delay, 0ms),
        transform var(--motion-reveal) var(--ease-out) var(--reveal-delay, 0ms);
}

@media (prefers-reduced-motion: reduce) {
    .is-reveal-ready [data-reveal] > * {
        opacity: 1;
        transform: none;
        transition: none;
    }
}

/* Set on <body> while the mobile menu overlay is open (js/header.js). */
.u-no-scroll {
    overflow: hidden;
}

.u-visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* Skip link (7.4, WCAG 2.4.1) — off-screen (not .u-visually-hidden's 1px
   clip: that variant has no focus exception, and this one needs to become
   fully visible the moment it's reached by keyboard). Sits above the fixed
   header (z-index 52) so a focused link is never hidden behind it. */
.skip-link {
    position: fixed;
    top: 0;
    left: var(--space-16);
    z-index: 9999;
    transform: translateY(-120%);
    padding: var(--space-16) var(--space-24);
    background: var(--white);
    color: var(--primary);
    border-radius: 0 0 var(--radius-sm) var(--radius-sm);
    box-shadow: 0 0 40px rgb(36 57 77 / .18);
    font-weight: 600;
    text-decoration: none;
    transition: transform var(--motion-micro);
}

.skip-link:focus-visible {
    transform: translateY(0);
}

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

/* Inline success state for the contact and callback forms (4.6). It replaces
   the form in place, so it carries its own card padding rather than relying
   on whatever wrapper it lands in. */
.form-success {
    margin: 0;
    padding: var(--space-24);
    border-radius: var(--radius-sm);
    background: var(--green-light);
    color: var(--primary);
    font-size: var(--text-base);
    line-height: 1.5;
}

.form-success:focus {
    outline: none;
}
