/* /bs widget styles.
   Follows the Figma: white card, dark inputs on light grey, big green submit.

   Theming: ?themeColor= overrides --brand, --brand-hover and --tint on #app at
   boot (see theme.ts). The values below are the DKV defaults and the shape the
   override relies on — every accented surface must keep reading from these
   three properties, or it will stay green on a themed widget. */

:root {
  --brand: #1a6b3c;
  --brand-hover: #14562f;
  --ink: #1a1a1a;
  --muted: #6b7280;
  --field-bg: #eceff1;
  --field-border: #d1d5db;
  --danger: #b3261e;

  /* From flutter-ui-kit AppColor.light — kept as raw RGB triples so the 30%
     alpha backgrounds can be built with color-mix-free rgb(... / 0.3), exactly
     as the Flutter component does. */
  --kit-green: 82, 210, 149;
  --kit-red: 237, 103, 95;
  --kit-orange: 248, 175, 67;
  --kit-dark-green: 5, 91, 91;
  --kit-info-grey: 234, 234, 234;
  /* The pale green-grey from the design, shared by dropdown hover and the
     dropzone's resting background so they read as one surface treatment.
     Derived from --brand when a customer themes the widget, so the wash always
     belongs to the same hue as the accent. */
  --tint: #f1f5f2;
  --kit-dark-grey: 56, 56, 56;
  --radius: 6px;
  --gap: 16px;
  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

* { box-sizing: border-box; }

/* Used by every spinner in the widget (infobox, thumbnails). Defined once,
   near the top, so removing a component's styles cannot orphan it again. */
@keyframes spin { to { transform: rotate(360deg); } }

body {
  margin: 0;
  padding: 20px;
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.5;
  color: var(--ink);
  background: #fff;
}

#app { max-width: 640px; margin: 0 auto; }

/* The `hidden` attribute works through a default `display: none` rule, which
   ANY explicit display declaration silently overrides — #state-challenge sets
   display:flex, so it stayed visible no matter what the state machine did.
   This makes hidden authoritative. */
[hidden] { display: none !important; }

h1 {
  /* Fluid rather than fixed: at 30px the Danish title wrapped to three lines on
     a narrow embed. Scales down to 22px and back up on wider screens. */
  font-size: clamp(22px, 5.5vw, 30px);
  line-height: 1.15;
  color: var(--brand);
  margin: 0 0 24px;
  font-weight: 700;
}

h2 {
  font-size: 20px;
  margin: 0 0 8px;
  color: var(--brand);
}

.muted { color: var(--muted); }
.small { font-size: 13px; }

/* --- fields --- */

.field { margin-bottom: var(--gap); }

label {
  display: block;
  font-size: 14px;
  font-weight: 600;
  margin-bottom: 6px;
}

input[type="text"],
input[type="email"],
input[type="tel"],
select,
textarea {
  width: 100%;
  font: inherit;
  color: inherit;
  padding: 12px 14px;
  border: 1px solid var(--field-border);
  border-radius: var(--radius);
  background: #fff;
}

select {
  background: var(--field-bg);
  border-color: transparent;
  /* Keep the native chevron and native picker on mobile — a custom dropdown
     would be worse for accessibility and for one-handed phone use. */
}

select:disabled {
  color: var(--muted);
  opacity: 1; /* iOS otherwise fades it to near-invisible */
}

textarea { resize: vertical; min-height: 120px; }

/* On focus the border BECOMES green rather than gaining an outline on top of
   the grey one, which read as a doubled border. */
input:focus,
textarea:focus {
  border-color: var(--brand);
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

button:focus-visible,
.combo-trigger:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* Chrome paints autofilled fields pale blue via its own UA stylesheet, which
   only affects fields it recognises (name, email, phone) - so the form ended up
   half blue, half white. There is no property to disable it; the standard
   workaround is an inset shadow large enough to cover the field, since the
   background itself cannot be overridden. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 100px #fff inset;
  box-shadow: 0 0 0 100px #fff inset;
  -webkit-text-fill-color: var(--ink);
}

input:disabled:-webkit-autofill {
  -webkit-box-shadow: 0 0 0 100px var(--field-bg) inset;
  box-shadow: 0 0 0 100px var(--field-bg) inset;
}

/* Locked while submitting: visibly inert, matching the disabled dropdowns. */
input:disabled,
textarea:disabled {
  background: var(--field-bg);
  color: var(--muted);
  cursor: default;
}

input[aria-invalid="true"],
select[aria-invalid="true"],
textarea[aria-invalid="true"] {
  border-color: var(--danger);
}

.error {
  color: var(--danger);
  font-size: 13px;
  margin: 6px 0 0;
}

/* --- buttons --- */

button {
  font: inherit;
  cursor: pointer;
  border-radius: var(--radius);
}

.primary {
  width: 100%;
  padding: 16px;
  font-size: 17px;
  font-weight: 600;
  color: #fff;
  background: var(--brand);
  border: none;
  margin-top: 8px;
}

.primary:hover:not(:disabled) { background: var(--brand-hover); }
.primary:disabled { opacity: 0.6; cursor: default; }

/* Secondary action: same brand colour as Submit but outlined, so it reads as
   part of the same system while staying clearly subordinate to it. */
.secondary {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 12px 18px;
  color: var(--brand);
  background: #fff;
  border: 2px solid var(--brand);
  font-weight: 600;
}

.secondary:hover:not(:disabled) {
  background: var(--brand);
  color: #fff;
}

/* Disabled state was missing entirely: the button looked identical whether or
   not it could be clicked. */
.secondary:disabled {
  color: var(--muted);
  border-color: var(--field-border);
  background: var(--field-bg);
  cursor: default;
}

/* Destructive action, marked as such at rest: red icon and red border, the way
   DKV's own delete buttons read. The label stays dark so the button is not
   shouting a whole sentence in red. */
.link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 14px;
  padding: 9px 16px;
  background: #fff;
  border: 1px solid rgb(var(--kit-red));
  border-radius: 8px;
  color: var(--ink);
  font-size: 14px;
  transition: color 150ms ease, border-color 150ms ease, background-color 150ms ease;
}

/* The icon carries the red at rest via currentColor on its own wrapper. */
.link svg { color: rgb(var(--kit-red)); transition: color 150ms ease; }
.link:hover:not(:disabled) svg { color: #fff; }

.link:disabled {
  opacity: 0.5;
  cursor: default;
}

.link:hover:not(:disabled) {
  color: #fff;
  border-color: rgb(var(--kit-red));
  background: rgb(var(--kit-red));
}

/* --- image section --- */

.images-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 6px;
}

.images-header label { margin: 0; }

/* Deliberately neutral at every count: 10 of 10 is a normal, successful state,
   not an error. The warning box speaks up only if an eleventh is attempted. */
.images-count { font-size: 14px; color: var(--muted); }

.dropzone {
  /* CSS `dashed` gives no control over dash length or gap - the browser picks,
     and picks short and tight. Drawing the border as an inline SVG makes the
     rhythm explicit (8px dash, 6px gap) and still respects rounded corners. */
  border: none;
  border-radius: 12px;
  padding: 28px 24px;
  text-align: center;
  /* background-color and background-image set separately: the `background`
     shorthand would reset the image and silently remove the dashed border. */
  background-color: var(--tint);
  cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25'%3E%3Crect width='100%25' height='100%25' fill='none' rx='12' ry='12' stroke='%23c8cdd2' stroke-width='2' stroke-dasharray='8 6'/%3E%3C/svg%3E");
  transition: background-color 150ms ease;
}

/* Feedback while a file is dragged over: without it there is no sign that
   dropping will do anything. Kept light — the design's resting state is a
   plain white card, so the drag state should read as a nudge, not a takeover. */
/* Matches the button it wraps: not clickable, and visibly so. */
.dropzone.is-disabled {
  cursor: default;
  opacity: 0.6;
}

/* Only the border colour changes on drag: the fill staying put keeps the card
   calm while still signalling that a drop will land. */
.dropzone.is-dragging {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25'%3E%3Crect width='100%25' height='100%25' fill='none' rx='12' ry='12' stroke='%231a6b3c' stroke-width='2' stroke-dasharray='8 6'/%3E%3C/svg%3E");
}

.dropzone-title {
  margin: 0 0 6px;
  font-size: 16px;
  font-weight: 700;
  color: var(--brand);
}

.dropzone-hint {
  margin: 0 0 16px;
  font-size: 14px;
  color: var(--muted);
}

/* Shorter than the full-width primary button: this is an inline action inside
   a card, not the form's main call to action. */
.dropzone-button {
  width: auto;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 12px 22px;
  font-size: 15px;
  margin-top: 0;
  /* Deliberately NOT white-space: nowrap. An unbreakable label forces the
     button - and with it the whole dropzone - wider than the viewport at small
     sizes, which clips the layout instead of merely looking tight. Wrapping is
     the lesser evil; max-width keeps the wrap sensible. */
  max-width: 100%;
}

/* --- thumbnails --- */

.thumbnails {
  list-style: none;
  margin: 16px 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 14px;
}

.thumb { position: relative; }

.thumb-image {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: 8px;
  overflow: hidden;
  background: var(--field-bg);
  border: 1px solid var(--field-border);
}

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

/* Filename under the tile — with the failure state covering the image, this is
   how the tenant identifies which file went wrong. */
.thumb-name {
  margin: 6px 0 0;
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.thumb-remove {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: none;
  /* Solid red at rest with a white bin: removal is destructive, and this is the
     one place in the form where that should be unmistakable. White on red is
     3.13:1 — a red icon on the green circle measured 2.09:1, below the 3:1
     minimum for icons, and red-on-green is the most common colour-blind pair. */
  background: rgb(var(--kit-red));
  color: #fff;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
  transition: background-color 150ms ease;
}

/* Red only on hover: removal is destructive, but the resting state should not
   shout at a tenant who is simply looking at their photos. */
.thumb-remove svg { width: 16px; height: 16px; display: block; }

/* The failure overlay covers the tile, so the remove button has to sit above it
   and keep its own circle — otherwise it renders as a bare white cross on red
   and there is nothing for hover to act on. */
/* The failure overlay is already red, so the button flips to white with a red
   bin — otherwise it disappears into the tile behind it. */
.thumb[data-failed="true"] .thumb-remove {
  z-index: 3;
  background: #fff;
  color: rgb(var(--kit-red));
}

.thumb[data-failed="true"] .thumb-remove:hover {
  background: #f3f4f6;
}

/* Darkens rather than changing hue, so hover reads as "yes, this one". */
.thumb-remove:hover:not(:disabled) { background: #d4453c; }

/* Disabled while the upload is in flight: there is no uploadId yet, so a
   removal at that moment could not be told to the backend. */
.thumb-remove:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Spinner centred over the image while the upload is in flight. */
.thumb-spinner {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 28px;
  height: 28px;
  margin: -14px 0 0 -14px;
  border: 3px solid rgba(255, 255, 255, 0.6);
  border-top-color: var(--brand);
  border-radius: 50%;
  animation: spin 700ms linear infinite;
}

/* Once uploaded or failed there is nothing left to wait for. */
.thumb[data-uploaded="true"] .thumb-spinner,
.thumb[data-failed="true"] .thumb-spinner { display: none; }

.thumb-progress {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 4px;
  background: var(--brand);
  transition: width 120ms linear;
  z-index: 1;
}

.thumb[data-uploaded="true"] .thumb-progress,
.thumb[data-failed="true"] .thumb-progress { display: none; }

/* Failure covers the whole tile, per the design. The filename below is what
   keeps the file identifiable. */
.thumb-failure {
  position: absolute;
  inset: 0;
  background: rgb(var(--kit-red));
  color: #fff;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 10px;
  text-align: center;
}

.thumb-failure p {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.25;
}

.thumb-retry {
  border: none;
  border-radius: 6px;
  background: #fff;
  color: rgb(var(--kit-red));
  font-size: 13px;
  font-weight: 600;
  padding: 7px 14px;
}

@media (prefers-reduced-motion: reduce) {
  .thumb-spinner { animation: none; opacity: 0.6; }
}

/* --- the inline security gate ---
   One slot under the phone field holding, in turn: a "preparing" spinner, the
   Turnstile challenge, and a "validating" spinner. It animates open and closed
   rather than appearing and vanishing, so the fields below never jump.

   Kept deliberately short (200ms): the widget lives in an iframe that reports
   its height to the parent on every change, so a long animation means a long
   stream of resize messages. */

/* Starts collapsed and takes no space at all: on the silent path the gate never
   shows anything until "Henter lokationer…", and an empty div still carrying
   its margin left a 16px hole under the phone field for the whole boot. Space
   is claimed by .is-open, when there is actually something in it. */
.gate {
  overflow: hidden;
  margin-bottom: 0;
  max-height: 0;
  opacity: 0;
  transition: max-height 200ms ease, opacity 200ms ease, margin-bottom 200ms ease;
}

/* Something to show: open up and join the layout. */
.gate.is-open {
  margin-bottom: var(--gap);
  max-height: 200px;
  opacity: 1;
}

/* Resolved: collapse away and stop taking part in the layout. */
.gate.is-done {
  max-height: 0;
  opacity: 0;
  margin-bottom: 0;
}

/* Result badge: a moment of confirmation before the gate disappears, so the
   tenant sees that their click landed rather than the form just changing. */
.gate-icon {
  display: inline-grid;
  place-items: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  font-size: 12px;
  line-height: 1;
  color: #fff;
  background: var(--brand);
}

.gate.is-failed .gate-icon { background: var(--danger); }
.gate.is-failed #gate-text { color: var(--danger); }

/* Space between the status box and the challenge widget below it. */
#turnstile-host:not(:empty) { margin-top: 12px; }

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

/* --- combobox (dropdown, per the design) ---
   White trigger with a green border, and the panel as a separate white card
   below it rather than a flush continuation. Locations carry a search field;
   addresses do not. */

.combo { position: relative; }

.combo-trigger {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 13px 16px;
  font: inherit;
  text-align: left;
  color: var(--ink);
  background: #fff;
  border: 1px solid var(--field-border);
  border-radius: 8px;
  transition: border-color 150ms ease;
}

.combo-trigger[aria-expanded="true"] { border-color: var(--brand); }
.combo-trigger[aria-invalid="true"] { border-color: var(--danger); }

.combo-trigger:disabled {
  color: var(--muted);
  border-color: var(--field-border);
  background: var(--field-bg);
  cursor: default;
}

.combo-value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.combo-value.is-placeholder { color: #4b5563; }

.combo-chevron {
  flex: none;
  width: 20px;
  height: 20px;
  display: block;
  color: var(--muted);
  /* transform-origin defaults to the box centre, and an SVG box has no
     baseline to shift it — so it rotates about its own axis. */
  transition: transform 180ms ease;
}

.combo-trigger[aria-expanded="true"] .combo-chevron { transform: rotate(180deg); }

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

.combo-panel {
  position: absolute;
  z-index: 20;
  left: 0;
  right: 0;
  top: calc(100% + 6px);
  background: #fff;
  border: 1px solid var(--field-border);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.10);
  padding: 8px;
}

.combo-search {
  width: 100%;
  padding: 13px 16px;
  font: inherit;
  border: 1px solid var(--field-border);
  border-radius: 8px;
  background: #fff;
  /* Divider so the search field reads as separate from the results below. */
  margin-bottom: 8px;
}

.combo-search:focus {
  border-color: var(--brand);
  outline: none;
}

.combo-panel:has(.combo-search) .combo-list {
  border-top: 1px solid var(--field-border);
  padding-top: 8px;
}

.combo-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 280px;
  overflow-y: auto;
  outline: none;
}

/* No scrollbar-gutter and no fixed right padding: both reserve space whether or
   not a scrollbar exists, which left a visible gap beside short lists. An
   overflowing list takes its own width for the scrollbar, so the rows shrink
   only when they actually need to. The small margin keeps the hover fill from
   touching the scrollbar when there is one. */
.combo-list::-webkit-scrollbar { width: 8px; }
.combo-list::-webkit-scrollbar-thumb {
  background: #c8cdd2;
  border-radius: 4px;
}
.combo-list::-webkit-scrollbar-track { background: transparent; }

.combo-option { margin-right: 2px; }

.combo-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px;
  border-radius: 6px;
  cursor: pointer;
}

/* Divider drawn as an inset background rather than a border, so it lines up
   with the hover fill's own left and right edges instead of running the full
   width of the list. */
.combo-option + .combo-option {
  background-image: linear-gradient(to right, var(--tint), var(--tint));
  background-size: calc(100% - 28px) 1px;
  background-position: 14px 0;
  background-repeat: no-repeat;
}

/* Hover and keyboard focus share one highlight, so the two navigation modes
   never disagree about which row is current. The `background` shorthand also
   clears the divider gradient above, so a hovered row shows fill instead of a
   line — which is what the design does. */
.combo-option:hover,
.combo-option[aria-selected="true"] { background: var(--tint); }

.combo-labels { min-width: 0; }
.combo-primary { display: block; font-weight: 600; }
.combo-secondary { display: block; font-size: 13px; color: var(--muted); }

.combo-check { flex: none; color: var(--brand); font-weight: 700; }

.combo-empty { padding: 16px 14px; color: var(--muted); }

/* --- infobox ---
   Mirrors the Flutter UI kit's InfoStatusBox so the widget matches the rest of
   DKV. Same 12px radius, same icon-left row, same colours.

   The pattern to preserve: the background is the status colour at 30% alpha and
   the TEXT is dark grey. The status colour is carried by the icon, not the
   words — that is what keeps these pale backgrounds readable. */

.infobox {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  padding: 16px;
  border-radius: 12px;
  margin: 10px 0 0;
  background: rgb(var(--kit-info-grey));
  color: rgb(var(--kit-dark-grey));
}

.infobox-body { min-width: 0; flex: 1; }

/* Offered only on retryable failures — see Infobox.show(). */
.infobox-action {
  margin-top: 10px;
  padding: 8px 14px;
  border: 1px solid currentColor;
  border-radius: 8px;
  background: #fff;
  color: rgb(var(--kit-dark-grey));
  font-size: 14px;
  font-weight: 600;
}

.infobox-action:hover {
  color: #fff;
  border-color: var(--brand);
  background: var(--brand);
}

.infobox-text {
  margin: 0;
  font-size: 15px;
  line-height: 1.4;
  /* Long server messages must wrap inside the box rather than stretch it. */
  overflow-wrap: anywhere;
}

.infobox-icon-slot {
  flex: none;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
}

.infobox-icon svg { width: 22px; height: 22px; display: block; }

/* info + loading: neutral grey box, dark green indicator */
.infobox.is-info .infobox-icon,
.infobox.is-loading .infobox-spinner { color: rgb(var(--kit-dark-green)); }

.infobox.is-success {
  background: rgba(var(--kit-green), 0.3);
}
.infobox.is-success .infobox-icon { color: rgb(var(--kit-green)); }

.infobox.is-error {
  background: rgba(var(--kit-red), 0.3);
}
.infobox.is-error .infobox-icon { color: rgb(var(--kit-red)); }

.infobox.is-warning {
  background: rgba(var(--kit-orange), 0.3);
}
.infobox.is-warning .infobox-icon { color: rgb(var(--kit-orange)); }

/* Loading spinner, matching the icon footprint. */
.infobox-spinner {
  width: 20px;
  height: 20px;
  border: 2px solid rgba(var(--kit-dark-green), 0.25);
  border-top-color: rgb(var(--kit-dark-green));
  border-radius: 50%;
  animation: spin 700ms linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .infobox-spinner { animation: none; opacity: 0.6; }
}

/* --- narrow embeds ---
   Down to the 320px floor. Everything here is typographic or spacing: the
   layout itself already reflows (fields stack, the thumbnail grid rewraps), so
   these only stop text and buttons from breaking awkwardly. */

@media (max-width: 400px) {
  body { padding: 12px; }

  /* Reclaim the horizontal space the dropzone spends on padding. */
  .dropzone { padding: 22px 14px; }
  .dropzone-button { padding: 12px 16px; }

  /* The hint reads as two lines here rather than three broken fragments. */
  .dropzone-hint { font-size: 13px; }

  /* Same reasoning as the dropzone button: no nowrap, since an unbreakable
     label would push the layout wider than the viewport. */
  .link { max-width: 100%; }

  /* The images header stacks rather than squeezing the counter. */
  .images-header { flex-wrap: wrap; gap: 2px; }

  /* "Søg på vej eller afdelingsnummer" needs ~210px and has ~248px here, so it
     fits but with little to spare. Trimming the padding buys back room rather
     than letting the placeholder clip. */
  .combo-search { padding: 12px 12px; font-size: 15px; }
  .combo-option { padding: 12px 10px; }

  /* Long location names (LOCATION_TEST_DELETE, "999 Herning Skrøder aaff")
     wrap rather than forcing the panel wider than its container. */
  .combo-primary { overflow-wrap: anywhere; }
}
