<template>
  <div class="billing-view">
    <div class="page-header">
      <div>
        <h2>{{ $t('billing.title') }}</h2>
        <p class="billing-subtitle">{{ $t('billing.subtitle') }}</p>
      </div>
    </div>
    <p v-if="feedback.message" class="billing-feedback" :class="`is-${feedback.type}`">
      {{ feedback.message }}
    </p>

    <div class="card billing-section">
      <p class="step-label">{{ $t('billing.step1') }}</p>
      <h3>{{ $t('billing.templateSection') }}</h3>
      <div class="form-grid">
        <div>
          <label>{{ $t('billing.templateName') }}</label>
          <input v-model="templateForm.name" type="text" />
        </div>
        <div>
          <label>{{ $t('billing.templateDescription') }}</label>
          <input v-model="templateForm.description" type="text" />
        </div>
        <div>
          <label>{{ $t('billing.templatePdf') }}</label>
          <input type="file" accept="application/pdf" @change="onTemplateFileChange" />
        </div>
      </div>
      <div class="actions">
        <button class="btn-primary" :disabled="loading.template" @click="uploadTemplate">
          {{ loading.template ? $t('common.saving') : $t('billing.uploadTemplate') }}
        </button>
      </div>
      <table class="billing-table templates-table">
        <thead>
          <tr>
            <th>{{ $t('billing.templateName') }}</th>
            <th>{{ $t('billing.templateDescription') }}</th>
            <th>{{ $t('common.actions') }}</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="template in templates" :key="template.id">
            <td>{{ template.name }} (v{{ template.version }})</td>
            <td>{{ template.description || '-' }}</td>
            <td class="row-actions">
              <button class="btn-danger" :disabled="loading.template" @click="deleteTemplate(template)">
                {{ $t('billing.deleteTemplate') }}
              </button>
            </td>
          </tr>
          <tr v-if="!templates.length">
            <td colspan="3">{{ $t('billing.noTemplates') }}</td>
          </tr>
        </tbody>
      </table>
      <p v-if="runForm.templateId" class="billing-hint">
        PDF-Formular erkannt: Die Felder werden direkt aus den Formularfeldern befuellt, ohne manuelles Positions-Mapping.
      </p>
    </div>

    <div class="card billing-section">
      <p class="step-label">{{ $t('billing.step2') }}</p>
      <h3>{{ $t('billing.runSection') }}</h3>
      <div class="form-grid">
        <div class="form-field">
          <label>{{ $t('billing.template') }}</label>
          <select v-model="runForm.templateId">
            <option value="">{{ $t('common.choose') }}</option>
            <option v-for="template in templates" :key="template.id" :value="template.id">
              {{ template.name }} (v{{ template.version }})
            </option>
          </select>
        </div>
        <div class="form-field">
          <label>{{ $t('billing.monthFrom') }}</label>
          <input v-model="runForm.monthFrom" type="month" />
        </div>
        <div class="form-field">
          <label>{{ $t('billing.monthTo') }}</label>
          <input v-model="runForm.monthTo" type="month" />
        </div>
        <div class="form-field">
          <label>{{ $t('billing.selfRecipientName') }}</label>
          <input v-model="runForm.selfRecipientName" type="text" />
          <label class="omit-check"><input v-model="runForm.omitSelfRecipientName" type="checkbox" /> {{ $t('billing.omitField') }}</label>
        </div>
        <div class="form-field">
          <label>{{ $t('billing.iban') }}</label>
          <input v-model="runForm.iban" type="text" :disabled="runForm.omitIban" />
          <div class="field-options">
            <label class="omit-check"><input v-model="runForm.ibanWithoutCountry" type="checkbox" /> {{ $t('billing.ibanWithoutCountry') }}</label>
            <label class="omit-check"><input v-model="runForm.omitIban" type="checkbox" /> {{ $t('billing.omitField') }}</label>
          </div>
        </div>
        <div class="form-field">
          <label>{{ $t('billing.hourlyRate') }}</label>
          <input v-model.number="runForm.hourlyRate" type="number" min="0" step="0.01" />
        </div>
        <div class="form-field">
          <label>{{ $t('billing.locationText') }}</label>
          <input v-model="runForm.locationText" type="text" />
          <label class="omit-check"><input v-model="runForm.omitLocationText" type="checkbox" /> {{ $t('billing.omitField') }}</label>
        </div>
        <div class="form-field">
          <label>{{ $t('billing.documentDate') }}</label>
          <input v-model="runForm.documentDate" type="date" />
          <label class="omit-check"><input v-model="runForm.omitDocumentDate" type="checkbox" /> {{ $t('billing.omitField') }}</label>
        </div>
        <div class="form-field">
          <label>{{ $t('billing.sessionLabel') }}</label>
          <input v-model="runForm.sessionLabel" type="text" />
          <label class="omit-check"><input v-model="runForm.omitSessionLabel" type="checkbox" /> {{ $t('billing.omitField') }}</label>
        </div>
        <div class="form-field same-account-field">
          <label>{{ $t('billing.sameAccountCheckbox') }}</label>
          <label class="omit-check"><input v-model="runForm.sameAccountCheckbox" type="checkbox" /> {{ $t('common.yes') }}</label>
        </div>
      </div>
      <div class="actions">
        <button class="btn-primary" :disabled="loading.run" @click="createRunAndGenerate">
          {{ loading.run ? $t('common.saving') : $t('billing.generateOwnBilling') }}
        </button>
      </div>
      <p class="billing-hint">
        {{ $t('billing.hoursAutoHint', { hours: computedHoursTotalDisplay, count: sessionRows.length }) }}
      </p>
      <table class="billing-table sessions-table">
        <thead>
          <tr>
            <th>{{ $t('common.date') }}</th>
            <th>{{ $t('billing.sessionLabel') }}</th>
            <th>{{ $t('billing.sessionHours') }}</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(row, idx) in sessionRows" :key="`${row.date}-${idx}`">
            <td>{{ row.date }}</td>
            <td>{{ runForm.omitSessionLabel ? '' : runForm.sessionLabel }}</td>
            <td>{{ Number(row.durationHours || 0).toFixed(2) }}</td>
          </tr>
          <tr v-if="!sessionRows.length">
            <td colspan="3">{{ $t('billing.noSessionsInRange') }}</td>
          </tr>
        </tbody>
      </table>
    </div>

    <div class="card billing-section">
      <p class="step-label">{{ $t('billing.step3') }}</p>
      <h3>{{ $t('billing.runsTitle') }}</h3>
      <table class="billing-table">
        <thead>
          <tr>
            <th>{{ $t('common.name') }}</th>
            <th>{{ $t('common.period') }}</th>
            <th>{{ $t('billing.hourlyRate') }}</th>
            <th>{{ $t('common.status') }}</th>
            <th>{{ $t('common.actions') }}</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="run in runs" :key="run.id">
            <td>{{ run.selfRecipientName }}</td>
            <td>{{ formatPeriod(run.periodStart, run.periodEnd) }}</td>
            <td>{{ formatCurrency(run.hourlyRate) }}</td>
            <td>{{ run.status }}</td>
            <td class="row-actions">
              <button class="btn-primary" :disabled="isGenerating(run.id)" @click="generateAndDownloadRun(run.id)">
                {{ isGenerating(run.id) ? $t('billing.generatingPdf') : $t('billing.generateAndDownloadPdf') }}
              </button>
              <button class="btn-danger" :disabled="isGenerating(run.id) || !isDeletable(run)" @click="deleteRun(run)">
                {{ $t('billing.deleteBilling') }}
              </button>
            </td>
          </tr>
          <tr v-if="!runs.length">
            <td colspan="5">{{ $t('billing.noRuns') }}</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex';
import { getDocument, GlobalWorkerOptions, OPS } from 'pdfjs-dist';
import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url';
import apiClient from '../apiClient.js';
import { safeLocalStorage } from '../utils/storage.js';

GlobalWorkerOptions.workerSrc = pdfWorkerUrl;

export default {
  name: 'BillingView',
  data() {
    const thisMonth = new Date().toISOString().slice(0, 7);
    return {
      templates: [],
      runs: [],
      templateForm: {
        name: '',
        description: '',
        file: null
      },
      runForm: {
        templateId: '',
        monthFrom: thisMonth,
        monthTo: thisMonth,
        selfRecipientName: '',
        iban: '',
        ibanWithoutCountry: false,
        hourlyRate: '',
        sessionLabel: '',
        sameAccountCheckbox: false,
        omitSelfRecipientName: false,
        omitIban: false,
        omitLocationText: false,
        omitDocumentDate: false,
        omitSessionLabel: false,
        locationText: '',
        documentDate: new Date().toISOString().slice(0, 10)
      },
      computedHoursTotal: null,
      sessionRows: [],
      loading: {
        template: false,
        run: false
      },
      generatingRunIds: [],
      feedback: {
        message: '',
        type: 'info'
      },
      mappingEditor: {
        open: false,
        selectedFieldKey: 'self_recipient_name',
        scale: 1,
        canvasHeight: 0,
        pageHeight: 0,
        textItems: [],
        geometryRects: [],
        ibanLearnMode: {
          active: false,
          step: 1,
          startIndex: null,
          startPoint: null
        }
      },
      mappingFields: [
        { key: 'self_recipient_name', label: 'Name', x: 45, y: 742, fontSize: 11 },
        { key: 'iban', label: 'IBAN (komplett)', x: 98, y: 613, fontSize: 11 },
        { key: 'iban_box_1', label: 'IBAN Box 1', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_2', label: 'IBAN Box 2', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_3', label: 'IBAN Box 3', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_4', label: 'IBAN Box 4', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_5', label: 'IBAN Box 5', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_6', label: 'IBAN Box 6', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_7', label: 'IBAN Box 7', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_8', label: 'IBAN Box 8', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_9', label: 'IBAN Box 9', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_10', label: 'IBAN Box 10', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_11', label: 'IBAN Box 11', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_12', label: 'IBAN Box 12', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_13', label: 'IBAN Box 13', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_14', label: 'IBAN Box 14', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_15', label: 'IBAN Box 15', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_16', label: 'IBAN Box 16', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_17', label: 'IBAN Box 17', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_18', label: 'IBAN Box 18', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_19', label: 'IBAN Box 19', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_20', label: 'IBAN Box 20', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_21', label: 'IBAN Box 21', x: null, y: null, fontSize: 11 },
        { key: 'iban_box_22', label: 'IBAN Box 22', x: null, y: null, fontSize: 11 },
        { key: 'location_text', label: 'Ort', x: 45, y: 430, fontSize: 11 },
        { key: 'document_date', label: 'Datum', x: 120, y: 430, fontSize: 11 },
        { key: 'same_account_checkbox', label: 'Gleiches Konto (X)', x: 158, y: 670, fontSize: 11 },
        { key: 'lessons_rows', label: 'Zeilen-Start', x: 40, y: 680, fontSize: 10 },
        { key: 'lessons_rows_date', label: 'Zeile Datum', x: 40, y: 680, fontSize: 10 },
        { key: 'lessons_rows_time', label: 'Zeile Bezeichner', x: 145, y: 680, fontSize: 10 },
        { key: 'lessons_rows_hours', label: 'Zeile Stunden', x: 275, y: 680, fontSize: 10 },
        { key: 'hours_total', label: 'Gesamtstunden', x: 70, y: 385, fontSize: 11 },
        { key: 'hours_total_2', label: 'Gesamtstunden (2)', x: 70, y: 365, fontSize: 11 },
        { key: 'hourly_rate', label: 'Stunden-Gehalt', x: 145, y: 385, fontSize: 11 },
        { key: 'amount_total', label: 'Gesamtbetrag', x: 240, y: 385, fontSize: 11 }
      ],
    };
  },
  watch: {
    username: {
      immediate: true,
      handler(newValue) {
        if (!this.runForm.selfRecipientName) {
          this.runForm.selfRecipientName = newValue || '';
        }
      }
    },
    currentClub: {
      immediate: true,
      handler() {
        this.loadData();
      }
    },
    'runForm.monthFrom'() {
      this.refreshHoursPreview();
    },
    'runForm.monthTo'() {
      this.refreshHoursPreview();
    },
    'runForm.selfRecipientName'() {
      this.saveDraftDefaults();
    },
    'runForm.locationText'() {
      this.saveDraftDefaults();
    },
    'runForm.sessionLabel'() {
      this.saveDraftDefaults();
    },
    'runForm.omitIban'(omit) {
      if (omit) {
        this.runForm.iban = '';
      }
    }
  },
  computed: {
    ...mapGetters(['currentClub', 'username']),
    computedHoursTotalDisplay() {
      if (this.computedHoursTotal == null) return '-';
      return `${Number(this.computedHoursTotal).toFixed(2)}`;
    },
    canvasMarkers() {
      const scale = Number(this.mappingEditor.scale || 1);
      const canvasHeight = Number(this.mappingEditor.canvasHeight || 0);
      return this.mappingFields
        .filter((field) => Number.isFinite(Number(field.x)) && Number.isFinite(Number(field.y)))
        .map((field) => ({
        key: field.key,
        label: field.label,
        left: Number(field.x || 0) * scale,
        top: canvasHeight - (Number(field.y || 0) * scale)
      }));
    }
  },
  methods: {
    getBillingDraftStorageKey() {
      return `billing:draft:${this.currentClub || 'none'}`;
    },
    loadDraftDefaults() {
      const raw = safeLocalStorage.getItem(this.getBillingDraftStorageKey());
      if (!raw) return;
      try {
        const parsed = JSON.parse(raw);
        if (typeof parsed?.selfRecipientName === 'string' && parsed.selfRecipientName.trim()) {
          this.runForm.selfRecipientName = parsed.selfRecipientName;
        }
        if (typeof parsed?.locationText === 'string' && parsed.locationText.trim()) {
          this.runForm.locationText = parsed.locationText;
        }
        if (typeof parsed?.sessionLabel === 'string' && parsed.sessionLabel.trim()) {
          this.runForm.sessionLabel = parsed.sessionLabel;
        }
      } catch (error) {
        /* noop */
      }
    },
    saveDraftDefaults() {
      const payload = {
        selfRecipientName: String(this.runForm.selfRecipientName || '').trim(),
        locationText: String(this.runForm.locationText || '').trim(),
        sessionLabel: String(this.runForm.sessionLabel || '').trim()
      };
      safeLocalStorage.setItem(this.getBillingDraftStorageKey(), JSON.stringify(payload));
    },
    async loadData() {
      if (!this.currentClub) return;
      this.loadDraftDefaults();
      await Promise.all([this.loadTemplates(), this.loadRuns(), this.loadSettings()]);
      await this.refreshHoursPreview();
    },
    async loadTemplates() {
      const response = await apiClient.get(`/billing/templates/${this.currentClub}`);
      if (response.data?.success) {
        this.templates = response.data.templates || [];
        if (!this.runForm.templateId && this.templates.length) {
          this.runForm.templateId = this.templates[0].id;
        }
      }
    },
    loadTemplateMapping(templateId) {
      const template = this.templates.find((t) => String(t.id) === String(templateId));
      if (!template) return;
      const byKey = {};
      (template.fields || []).forEach((f) => {
        byKey[f.fieldKey] = f;
      });
      this.mappingFields = this.mappingFields.map((m) => ({
        ...m,
        x: byKey[m.key]?.x != null ? Number(byKey[m.key].x) : (m.x ?? null),
        y: byKey[m.key]?.y != null ? Number(byKey[m.key].y) : (m.y ?? null),
        fontSize: byKey[m.key]?.fontSize != null ? Number(byKey[m.key].fontSize) : m.fontSize
      }));
    },
    async saveTemplateMapping() {
      if (!this.runForm.templateId) return;
      const fields = this.mappingFields.map((m, idx) => ({
        fieldKey: m.key,
        label: m.label,
        fieldType: m.key.startsWith('lessons_rows') ? 'table_row' : 'text',
        sourceType: 'manual',
        mappingMode: 'overlay',
        x: Number.isFinite(Number(m.x)) ? Number(m.x) : null,
        y: Number.isFinite(Number(m.y)) ? Number(m.y) : null,
        fontSize: Number(m.fontSize || 11),
        sortOrder: idx,
        height: m.key === 'lessons_rows' ? 12 : null
      }));
      try {
        const response = await apiClient.put(`/billing/templates/${this.runForm.templateId}/fields`, { fields });
        if (response.data?.success) {
          this.feedback = { message: this.$t('billing.mappingSaved'), type: 'success' };
          await this.loadTemplates();
        } else {
          this.feedback = { message: this.$t('billing.mappingSaveError'), type: 'error' };
        }
      } catch (error) {
        this.feedback = { message: this.$t('billing.mappingSaveError'), type: 'error' };
      }
    },
    async openMappingEditor() {
      if (!this.runForm.templateId) return;
      this.mappingEditor.open = true;
      this.feedback = { message: '', type: 'info' };
      try {
        await this.$nextTick();
        const response = await apiClient.get(`/billing/templates/${this.runForm.templateId}/pdf`, {
          responseType: 'arraybuffer'
        });
        const pdf = await getDocument({ data: response.data }).promise;
        const page = await pdf.getPage(1);
        const baseViewport = page.getViewport({ scale: 1 });
        const viewport = page.getViewport({ scale: 1.25 });
        const textContent = await page.getTextContent();
        const operatorList = await page.getOperatorList();
        this.mappingEditor.textItems = (textContent?.items || []).map((item) => ({
          text: String(item.str || ''),
          x: Number(item.transform?.[4] || 0),
          y: Number(item.transform?.[5] || 0)
        }));
        this.mappingEditor.pageHeight = Number(baseViewport.height || 0);
        this.mappingEditor.geometryRects = this.extractRectanglesFromOperatorList(operatorList);
        const canvas = this.$refs.mappingCanvas;
        const ctx = canvas.getContext('2d');
        canvas.width = viewport.width;
        canvas.height = viewport.height;
        canvas.style.width = `${viewport.width}px`;
        canvas.style.height = `${viewport.height}px`;
        this.mappingEditor.scale = viewport.width / baseViewport.width;
        this.mappingEditor.canvasHeight = viewport.height;
        await page.render({ canvasContext: ctx, viewport }).promise;
      } catch (error) {
        this.feedback = { message: this.$t('billing.mappingPreviewError'), type: 'error' };
      }
    },
    extractRectanglesFromOperatorList(operatorList) {
      const rects = [];
      if (!operatorList?.fnArray || !operatorList?.argsArray) return rects;
      for (let i = 0; i < operatorList.fnArray.length; i += 1) {
        const fn = operatorList.fnArray[i];
        const args = operatorList.argsArray[i];
        if (fn !== OPS.constructPath || !Array.isArray(args) || args.length < 2) continue;
        const ops = args[0];
        const coords = args[1];
        if (!Array.isArray(ops) || !Array.isArray(coords)) continue;
        let coordIdx = 0;
        for (const op of ops) {
          if (op === OPS.rectangle) {
            const x = Number(coords[coordIdx] ?? 0);
            const y = Number(coords[coordIdx + 1] ?? 0);
            const w = Number(coords[coordIdx + 2] ?? 0);
            const h = Number(coords[coordIdx + 3] ?? 0);
            coordIdx += 4;
            if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(w) || !Number.isFinite(h)) continue;
            if (Math.abs(w) < 4 || Math.abs(h) < 4) continue;
            rects.push({ x, y, w: Math.abs(w), h: Math.abs(h) });
          } else if (op === OPS.moveTo || op === OPS.lineTo) {
            coordIdx += 2;
          } else if (op === OPS.curveTo) {
            coordIdx += 6;
          } else if (op === OPS.curveTo2 || op === OPS.curveTo3) {
            coordIdx += 4;
          } else if (op === OPS.closePath) {
            // no coords
          }
        }
      }
      return rects;
    },
    detectIbanBoxesFromGeometry(ibanAnchor) {
      const rects = this.mappingEditor.geometryRects || [];
      if (!rects.length || !ibanAnchor) return [];
      const anchorY = Number(ibanAnchor.y || 0);
      const anchorX = Number(ibanAnchor.x || 0);
      const candidates = rects.filter((r) => {
        const centerY = r.y + (r.h / 2);
        const centerX = r.x + (r.w / 2);
        return (
          r.w >= 8 && r.w <= 24 &&
          r.h >= 10 && r.h <= 30 &&
          Math.abs(centerY - anchorY) <= 30 &&
          centerX > anchorX + 20
        );
      });
      if (!candidates.length) return [];
      const rows = [];
      for (const rect of candidates) {
        const cy = rect.y + (rect.h / 2);
        let row = rows.find((entry) => Math.abs(entry.cy - cy) <= 8);
        if (!row) {
          row = { cy, items: [] };
          rows.push(row);
        }
        row.items.push(rect);
      }
      rows.sort((a, b) => b.items.length - a.items.length);
      const bestCy = rows[0]?.cy;
      const mergedRow = rows
        .filter((row) => Math.abs((row.cy || 0) - (bestCy || 0)) <= 14)
        .flatMap((row) => row.items || []);
      const raw = mergedRow
        .sort((a, b) => (a.x + a.w / 2) - (b.x + b.w / 2))
        .map((r) => ({
          x: Number((r.x + (r.w / 2) - 3).toFixed(1)),
          y: Number((r.y + (r.h / 2) - 4).toFixed(1)),
          w: Number(r.w || 0)
        }));

      // dedupe almost identical hits (some PDFs produce duplicates)
      const deduped = [];
      for (const box of raw) {
        const prev = deduped[deduped.length - 1];
        if (!prev || Math.abs(prev.x - box.x) > 3) {
          deduped.push(box);
        }
      }

      // recover up to two missed boxes inside blocks
      const widths = deduped.map((b) => b.w).filter((v) => v > 0).sort((a, b) => a - b);
      const medianWidth = widths.length ? widths[Math.floor(widths.length / 2)] : 12;
      const steps = [];
      for (let i = 1; i < deduped.length; i += 1) {
        const dx = deduped[i].x - deduped[i - 1].x;
        if (dx > 2 && dx < medianWidth * 1.9) steps.push(dx);
      }
      const baseStep = steps.length ? steps.sort((a, b) => a - b)[Math.floor(steps.length / 2)] : (medianWidth + 2);
      const recovered = [];
      for (let i = 0; i < deduped.length; i += 1) {
        recovered.push(deduped[i]);
        if (i === deduped.length - 1) continue;
        const curr = deduped[i];
        const next = deduped[i + 1];
        const dx = next.x - curr.x;
        if (dx > (baseStep * 1.6)) {
          const missingCount = Math.min(2, Math.max(1, Math.round(dx / baseStep) - 1));
          for (let m = 1; m <= missingCount; m += 1) {
            recovered.push({
              x: Number((curr.x + (baseStep * m)).toFixed(1)),
              y: Number(curr.y.toFixed(1)),
              w: curr.w
            });
          }
        }
      }

      return recovered.slice(0, 24).map(({ x, y }) => ({ x, y }));
    },
    detectIbanBoxesFromCanvas(ibanAnchor) {
      const canvas = this.$refs.mappingCanvas;
      if (!canvas || !ibanAnchor) return [];
      const ctx = canvas.getContext('2d', { willReadFrequently: true });
      if (!ctx) return [];
      const scale = Number(this.mappingEditor.scale || 1);
      if (!scale) return [];

      const anchorCanvasX = Math.max(0, Math.round(Number(ibanAnchor.x || 0) * scale));
      const anchorCanvasY = Math.max(0, Math.round(canvas.height - (Number(ibanAnchor.y || 0) * scale)));
      const yTop = Math.max(0, anchorCanvasY - 18);
      const yBottom = Math.min(canvas.height - 1, anchorCanvasY + 38);
      const bandHeight = Math.max(1, yBottom - yTop + 1);
      const image = ctx.getImageData(0, yTop, canvas.width, bandHeight);
      const data = image.data;

      const darkPerColumn = new Array(canvas.width).fill(0);
      for (let x = 0; x < canvas.width; x += 1) {
        let dark = 0;
        for (let y = 0; y < bandHeight; y += 1) {
          const idx = ((y * canvas.width) + x) * 4;
          const r = data[idx];
          const g = data[idx + 1];
          const b = data[idx + 2];
          const luminance = (0.2126 * r) + (0.7152 * g) + (0.0722 * b);
          if (luminance < 110) dark += 1;
        }
        darkPerColumn[x] = dark;
      }

      const minDark = Math.max(3, Math.floor(bandHeight * 0.18));
      const verticalStrokes = [];
      let runStart = null;
      for (let x = anchorCanvasX + 10; x < canvas.width; x += 1) {
        if (darkPerColumn[x] >= minDark) {
          if (runStart == null) runStart = x;
        } else if (runStart != null) {
          verticalStrokes.push({ start: runStart, end: x - 1, width: (x - runStart) });
          runStart = null;
        }
      }
      if (runStart != null) {
        verticalStrokes.push({ start: runStart, end: canvas.width - 1, width: (canvas.width - runStart) });
      }

      // Keep narrow vertical strokes that likely represent grid borders
      const borderLines = verticalStrokes
        .filter((s) => s.width <= Math.max(8, Math.round(scale * 6)))
        .map((s) => Math.round((s.start + s.end) / 2));
      if (borderLines.length < 6) return [];

      const boxes = [];
      for (let i = 1; i < borderLines.length; i += 1) {
        const left = borderLines[i - 1];
        const right = borderLines[i];
        const gap = right - left;
        if (gap < Math.round(scale * 8) || gap > Math.round(scale * 26)) continue;
        // Check that top/bottom horizontal segments exist inside the box
        const midX = Math.round((left + right) / 2);
        let darkAtMid = 0;
        for (let y = 0; y < bandHeight; y += 1) {
          const idx = ((y * canvas.width) + midX) * 4;
          const r = data[idx];
          const g = data[idx + 1];
          const b = data[idx + 2];
          const luminance = (0.2126 * r) + (0.7152 * g) + (0.0722 * b);
          if (luminance < 110) darkAtMid += 1;
        }
        if (darkAtMid < Math.max(2, Math.floor(bandHeight * 0.08))) continue;
        boxes.push({
          x: Number((((left + right) / 2) / scale).toFixed(1)),
          y: Number(((canvas.height - (yTop + Math.round(bandHeight * 0.55))) / scale).toFixed(1))
        });
      }

      // De-duplicate near-identical centers
      const deduped = [];
      for (const box of boxes) {
        const prev = deduped[deduped.length - 1];
        if (!prev || Math.abs(prev.x - box.x) > 2.2) deduped.push(box);
      }
      return deduped.slice(0, 24);
    },
    getUsableIbanBoxes() {
      const iban = this.findTextAnchor('iban');
      if (!iban) return [];
      const geometryBoxes = this.detectIbanBoxesFromGeometry(iban);
      const canvasBoxes = this.detectIbanBoxesFromCanvas(iban);
      const boxes = (canvasBoxes.length >= geometryBoxes.length ? canvasBoxes : geometryBoxes)
        .slice()
        .sort((a, b) => Number(a.x || 0) - Number(b.x || 0));
      if (!boxes.length) return [];
      const deAnchor = this.findTextAnchor('d e');
      let filtered = boxes.slice();
      if (deAnchor) {
        // Hard DE protection: usable IBAN boxes must never start left of the 3rd detected box.
        const thirdDetectedX = filtered.length >= 3 ? Number(filtered[2].x || 0) : Number.POSITIVE_INFINITY;
        // Keep everything clearly right of D/E label and also right of the third detected candidate.
        const semanticMinX = Number(deAnchor.x || 0) + 45;
        const hardMinX = Math.max(semanticMinX, thirdDetectedX);
        filtered = filtered.filter((b) => Number(b.x || 0) >= hardMinX);
      }
      const targetCount = deAnchor ? 20 : 22;
      let usable = filtered.slice(0, targetCount);
      // Only fallback when DE anchor is NOT found.
      // Otherwise we'd accidentally re-include D/E boxes.
      if (usable.length < 22 && boxes.length >= 22 && !deAnchor) {
        usable = boxes.slice(0, 22);
      }
      return usable;
    },
    getNearestBoxIndex(clickPdfX, clickPdfY, boxes) {
      let bestIdx = -1;
      let bestDist = Number.POSITIVE_INFINITY;
      for (let i = 0; i < boxes.length; i += 1) {
        const b = boxes[i];
        const dx = Number(b.x) - clickPdfX;
        const dy = Number(b.y) - clickPdfY;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < bestDist) {
          bestDist = dist;
          bestIdx = i;
        }
      }
      return bestIdx;
    },
    applyIbanLearnRange(startIdx, endIdx, boxes) {
      const deAnchor = this.findTextAnchor('d e');
      const targetCount = deAnchor ? 20 : 22;
      const s = Math.min(startIdx, endIdx);
      const e = Math.max(startIdx, endIdx);
      const selected = boxes.slice(s, e + 1).slice(0, targetCount);
      for (let i = 1; i <= 22; i += 1) {
        const key = `iban_box_${i}`;
        const item = selected[i - 1];
        if (item) this.setMappingField(key, item.x, item.y);
        else this.setMappingField(key, null, null);
      }
      if (selected.length) {
        this.setMappingField('iban', selected[0].x, selected[0].y);
      }
    },
    applyIbanLearnRangeByPoints(startPoint, endPoint) {
      if (!startPoint || !endPoint) return;
      const deAnchor = this.findTextAnchor('d e');
      const steps = deAnchor ? 20 : 22;
      const dx = (Number(endPoint.x) - Number(startPoint.x)) / Math.max(1, steps - 1);
      const dy = (Number(endPoint.y) - Number(startPoint.y)) / Math.max(1, steps - 1);
      for (let i = 0; i < steps; i += 1) {
        const x = Number((Number(startPoint.x) + (dx * i)).toFixed(1));
        const y = Number((Number(startPoint.y) + (dy * i)).toFixed(1));
        this.setMappingField(`iban_box_${i + 1}`, x, y);
      }
      for (let i = steps + 1; i <= 22; i += 1) {
        this.setMappingField(`iban_box_${i}`, null, null);
      }
      this.setMappingField('iban', Number(startPoint.x), Number(startPoint.y));
    },
    toggleIbanLearnMode() {
      const mode = this.mappingEditor.ibanLearnMode;
      mode.active = !mode.active;
      mode.step = 1;
      mode.startIndex = null;
      mode.startPoint = null;
      if (mode.active) {
        this.mappingEditor.selectedFieldKey = 'iban_box_1';
      }
    },
    findTextAnchor(...needles) {
      const items = this.mappingEditor.textItems || [];
      if (!items.length) return null;
      const lowered = needles.map((n) => String(n).toLowerCase());
      return items.find((item) => {
        const text = String(item.text || '').toLowerCase();
        return lowered.some((needle) => text.includes(needle));
      }) || null;
    },
    setMappingField(fieldKey, x, y) {
      const field = this.mappingFields.find((f) => f.key === fieldKey);
      if (!field) return;
      const nextX = Number(x);
      const nextY = Number(y);
      if (x === null) field.x = null;
      else if (Number.isFinite(nextX)) field.x = Number(nextX.toFixed(1));
      if (y === null) field.y = null;
      else if (Number.isFinite(nextY)) field.y = Number(nextY.toFixed(1));
    },
    getMappedIbanBoxes(exceptKey = null) {
      return this.mappingFields
        .filter((f) => /^iban_box_\d{1,2}$/.test(String(f.key)) && f.key !== exceptKey)
        .filter((f) => Number.isFinite(Number(f.x)) && Number.isFinite(Number(f.y)))
        .map((f) => ({ x: Number(f.x), y: Number(f.y) }));
    },
    snapToNearestIbanBox(clickPdfX, clickPdfY, currentKey) {
      const iban = this.findTextAnchor('iban');
      if (!iban) return null;
      const boxes = this.detectIbanBoxesFromGeometry(iban);
      if (!boxes.length) return null;
      const alreadyMapped = this.getMappedIbanBoxes(currentKey);
      const available = boxes.filter((b) => {
        return !alreadyMapped.some((m) => Math.abs(m.x - b.x) <= 3 && Math.abs(m.y - b.y) <= 3);
      });
      const pool = available.length ? available : boxes;
      let best = null;
      let bestDist = Number.POSITIVE_INFINITY;
      for (const box of pool) {
        const dx = Number(box.x) - clickPdfX;
        const dy = Number(box.y) - clickPdfY;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < bestDist) {
          bestDist = dist;
          best = box;
        }
      }
      return best;
    },
    async suggestMapping() {
      if (!this.runForm.templateId) return;
      if (!this.mappingEditor.textItems.length) {
        await this.openMappingEditor();
      }
      const pageHeight = Number(this.mappingEditor.pageHeight || 0);
      if (!pageHeight) {
        this.feedback = { message: this.$t('billing.mappingSuggestError'), type: 'error' };
        return;
      }

      const trainer = this.findTextAnchor('übungsleiter', 'uebungsleiter');
      if (trainer) {
        this.setMappingField('self_recipient_name', trainer.x + 2, trainer.y - 18);
      }

      const iban = this.findTextAnchor('iban');
      if (iban) {
        const boxes = this.getUsableIbanBoxes();
        if (boxes.length >= 8) {
          let usableBoxes = boxes;
          if (usableBoxes.length) {
            this.setMappingField('iban', usableBoxes[0].x, usableBoxes[0].y);
          }
          usableBoxes.forEach((box, idx) => {
            this.setMappingField(`iban_box_${idx + 1}`, box.x, box.y);
          });
        } else {
          const startX = iban.x + 45;
          const startY = iban.y + 1;
          this.setMappingField('iban', startX, startY);
          for (let i = 1; i <= 22; i += 1) {
            this.setMappingField(`iban_box_${i}`, startX + ((i - 1) * 13), startY);
          }
        }
      }

      const ortDatum = this.findTextAnchor('ort, datum');
      if (ortDatum) {
        this.setMappingField('location_text', ortDatum.x, ortDatum.y + 2);
        this.setMappingField('document_date', ortDatum.x + 70, ortDatum.y + 2);
      }

      const tableHeader = this.findTextAnchor('dauer in stunden');
      if (tableHeader) {
        const rowY = tableHeader.y - 28;
        this.setMappingField('lessons_rows', 40, rowY);
        this.setMappingField('lessons_rows_date', 40, rowY);
        this.setMappingField('lessons_rows_time', 145, rowY);
        this.setMappingField('lessons_rows_hours', 275, rowY);
      }

      const summary = this.findTextAnchor('ü-stdn', 'u-stdn');
      if (summary) {
        this.setMappingField('hours_total', summary.x + 2, summary.y + 2);
        this.setMappingField('hours_total_2', summary.x + 2, summary.y - 18);
        this.setMappingField('hourly_rate', summary.x + 72, summary.y + 2);
        this.setMappingField('amount_total', summary.x + 170, summary.y + 2);
      }

      const sameAccount = this.findTextAnchor('gleiches konto');
      if (sameAccount) {
        this.setMappingField('same_account_checkbox', sameAccount.x + 185, sameAccount.y + 2);
      }

      this.feedback = { message: this.$t('billing.mappingSuggested'), type: 'success' };
    },
    closeMappingEditor() {
      this.mappingEditor.open = false;
    },
    handleCanvasClick(event) {
      const canvas = this.$refs.mappingCanvas;
      if (!canvas) return;
      const selected = this.mappingFields.find((f) => f.key === this.mappingEditor.selectedFieldKey);
      if (!selected) return;
      const rect = canvas.getBoundingClientRect();
      const scaleX = canvas.width / rect.width;
      const scaleY = canvas.height / rect.height;
      const clickX = (event.clientX - rect.left) * scaleX;
      const clickY = (event.clientY - rect.top) * scaleY;
      const scale = Number(this.mappingEditor.scale || 1);
      const pdfX = Number((clickX / scale).toFixed(1));
      const pdfY = Number(((canvas.height - clickY) / scale).toFixed(1));
      if (this.mappingEditor.ibanLearnMode.active) {
        const mode = this.mappingEditor.ibanLearnMode;
        const boxes = this.getUsableIbanBoxes();
        const nearestIdx = boxes.length ? this.getNearestBoxIndex(pdfX, pdfY, boxes) : -1;
        if (mode.step === 1) {
          mode.startPoint = { x: pdfX, y: pdfY };
          mode.startIndex = nearestIdx >= 0 ? nearestIdx : null;
          mode.step = 2;
          this.mappingEditor.selectedFieldKey = this.findTextAnchor('d e') ? 'iban_box_20' : 'iban_box_22';
          this.feedback = { message: this.$t('billing.ibanLearnStep2'), type: 'info' };
          return;
        }
        const endPoint = { x: pdfX, y: pdfY };
        if (boxes.length && mode.startIndex != null && nearestIdx >= 0) {
          this.applyIbanLearnRange(mode.startIndex, nearestIdx, boxes);
        } else {
          this.applyIbanLearnRangeByPoints(mode.startPoint, endPoint);
        }
        mode.active = false;
        mode.step = 1;
        mode.startIndex = null;
        mode.startPoint = null;
        this.mappingEditor.selectedFieldKey = 'iban_box_1';
        this.feedback = { message: this.$t('billing.ibanLearnDone'), type: 'success' };
        return;
      }
      const ibanMatch = String(selected.key || '').match(/^iban_box_(\d{1,2})$/);
      if (ibanMatch) {
        const snapped = this.snapToNearestIbanBox(pdfX, pdfY, selected.key);
        if (snapped) {
          selected.x = Number(snapped.x.toFixed(1));
          selected.y = Number(snapped.y.toFixed(1));
        } else {
          selected.x = pdfX;
          selected.y = pdfY;
        }
      } else {
        selected.x = pdfX;
        selected.y = pdfY;
      }
      if (ibanMatch) {
        const next = Number(ibanMatch[1]) + 1;
        if (next <= 22) {
          // Keep manual IBAN mapping on one horizontal row.
          const currentY = selected.y;
          const nextField = this.mappingFields.find((f) => f.key === `iban_box_${next}`);
          if (nextField && !Number.isFinite(Number(nextField.y))) {
            nextField.y = currentY;
          }
          this.mappingEditor.selectedFieldKey = `iban_box_${next}`;
        }
      }
    },
    resetIbanBoxes() {
      this.mappingFields = this.mappingFields.map((field) => {
        if (/^iban_box_\d{1,2}$/.test(String(field.key))) {
          return { ...field, x: null, y: null };
        }
        return field;
      });
      this.feedback = { message: this.$t('billing.ibanBoxesReset'), type: 'success' };
    },
    nudgeSelectedField(dx, dy) {
      const selected = this.mappingFields.find((f) => f.key === this.mappingEditor.selectedFieldKey);
      if (!selected) return;
      selected.x = Number((Number(selected.x || 0) + dx).toFixed(1));
      selected.y = Number((Number(selected.y || 0) + dy).toFixed(1));
    },
    async loadRuns() {
      const response = await apiClient.get(`/billing/runs/${this.currentClub}`);
      if (response.data?.success) {
        this.runs = response.data.runs || [];
      }
    },
    async loadSettings() {
      const response = await apiClient.get(`/billing/settings/${this.currentClub}`);
      if (!response.data?.success || !response.data?.settings) return;
      const settings = response.data.settings;
      if (settings.lastHourlyRate != null && !this.runForm.hourlyRate) {
        this.runForm.hourlyRate = Number(settings.lastHourlyRate);
      }
      if (settings.lastSelfRecipientName) {
        this.runForm.selfRecipientName = settings.lastSelfRecipientName;
      }
      if (settings.lastLocationText) {
        this.runForm.locationText = settings.lastLocationText;
      }
      this.saveDraftDefaults();
    },
    async refreshHoursPreview() {
      if (!this.currentClub || !this.runForm.monthFrom || !this.runForm.monthTo) {
        this.computedHoursTotal = null;
        this.sessionRows = [];
        return;
      }
      const response = await apiClient.get(`/billing/hours-preview/${this.currentClub}`, {
        params: { monthFrom: this.runForm.monthFrom, monthTo: this.runForm.monthTo }
      });
      if (response.data?.success) {
        this.computedHoursTotal = Number(response.data.computedHoursTotal || 0);
        this.sessionRows = Array.isArray(response.data.sessions) ? response.data.sessions : [];
      }
    },
    onTemplateFileChange(event) {
      const file = event?.target?.files?.[0] || null;
      this.templateForm.file = file;
    },
    async uploadTemplate() {
      if (!this.currentClub || !this.templateForm.file || !this.templateForm.name.trim()) return;
      this.loading.template = true;
      try {
        const formData = new FormData();
        formData.append('name', this.templateForm.name.trim());
        formData.append('description', this.templateForm.description || '');
        formData.append('templatePdf', this.templateForm.file);
        await apiClient.post(`/billing/templates/${this.currentClub}`, formData, {
          headers: { 'Content-Type': 'multipart/form-data' }
        });
        this.templateForm = { name: '', description: '', file: null };
        await this.loadTemplates();
      } finally {
        this.loading.template = false;
      }
    },
    async deleteTemplate(template) {
      if (!template?.id) return;
      const ok = window.confirm(this.$t('billing.deleteTemplateConfirm'));
      if (!ok) return;
      this.loading.template = true;
      try {
        await apiClient.delete(`/billing/templates/${template.id}`);
        await this.loadTemplates();
        if (String(this.runForm.templateId) === String(template.id)) {
          this.runForm.templateId = this.templates.length ? this.templates[0].id : '';
        }
        this.feedback = { message: this.$t('billing.deleteTemplateSuccess'), type: 'success' };
      } catch (error) {
        this.feedback = { message: error?.response?.data?.error || this.$t('billing.deleteTemplateError'), type: 'error' };
      } finally {
        this.loading.template = false;
      }
    },
    async createRunAndGenerate() {
      if (!this.currentClub || !this.runForm.templateId) return;
      this.loading.run = true;
      this.feedback = { message: '', type: 'info' };
      try {
        const createResponse = await apiClient.post(`/billing/runs/${this.currentClub}`, {
          templateId: this.runForm.templateId,
          monthFrom: this.runForm.monthFrom,
          monthTo: this.runForm.monthTo,
          selfRecipientName: this.runForm.selfRecipientName,
          iban: this.runForm.iban,
          ibanWithoutCountry: this.runForm.ibanWithoutCountry,
          hourlyRate: this.runForm.hourlyRate,
          sessionLabel: this.runForm.sessionLabel,
          sameAccountCheckbox: this.runForm.sameAccountCheckbox,
          omitSelfRecipientName: this.runForm.omitSelfRecipientName,
          omitIban: this.runForm.omitIban,
          omitLocationText: this.runForm.omitLocationText,
          omitDocumentDate: this.runForm.omitDocumentDate,
          omitSessionLabel: this.runForm.omitSessionLabel,
          locationText: this.runForm.locationText,
          documentDate: this.runForm.documentDate
        });
        const runId = createResponse.data?.run?.id;
        if (runId) {
          const locale = this.$i18n?.locale || 'de-DE';
          const generateResponse = await apiClient.post(`/billing/runs/${runId}/generate`, { locale });
          if (!generateResponse.data?.success) {
            throw new Error(generateResponse.data?.error || 'PDF-Erzeugung fehlgeschlagen');
          }
          await this.downloadRunPdf(runId);
        }
        await this.loadRuns();
        this.saveDraftDefaults();
        this.feedback = { message: this.$t('billing.generateSuccess'), type: 'success' };
      } catch (error) {
        this.feedback = { message: this.$t('billing.generateError'), type: 'error' };
      } finally {
        this.loading.run = false;
      }
    },
    async generateExistingRun(runId) {
      if (this.isGenerating(runId)) return;
      this.generatingRunIds.push(runId);
      this.feedback = { message: '', type: 'info' };
      try {
        const locale = this.$i18n?.locale || 'de-DE';
        const response = await apiClient.post(`/billing/runs/${runId}/generate`, { locale });
        if (!response.data?.success) {
          throw new Error(response.data?.error || 'PDF-Erzeugung fehlgeschlagen');
        }
        await this.loadRuns();
        this.feedback = { message: this.$t('billing.generateSuccess'), type: 'success' };
      } catch (error) {
        this.feedback = { message: this.$t('billing.generateError'), type: 'error' };
      } finally {
        this.generatingRunIds = this.generatingRunIds.filter((id) => id !== runId);
      }
    },
    async generateAndDownloadRun(runId) {
      if (this.isGenerating(runId)) return;
      this.generatingRunIds.push(runId);
      this.feedback = { message: '', type: 'info' };
      try {
        const locale = this.$i18n?.locale || 'de-DE';
        const response = await apiClient.post(`/billing/runs/${runId}/generate`, { locale });
        if (!response.data?.success) {
          throw new Error(response.data?.error || 'PDF-Erzeugung fehlgeschlagen');
        }
        await this.downloadRunPdf(runId);
        await this.loadRuns();
        this.feedback = { message: this.$t('billing.generateSuccess'), type: 'success' };
      } catch (error) {
        this.feedback = { message: error?.response?.data?.error || this.$t('billing.generateError'), type: 'error' };
      } finally {
        this.generatingRunIds = this.generatingRunIds.filter((id) => id !== runId);
      }
    },
    isGenerating(runId) {
      return this.generatingRunIds.includes(runId);
    },
    isDeletable(run) {
      const status = String(run?.status || '').toLowerCase();
      return status === 'generated' || status === 'draft' || status === 'error';
    },
    parseResolvedValues(values = []) {
      const map = {};
      for (const entry of values) {
        map[entry.fieldKey] = entry.resolvedValue;
      }
      return map;
    },
    async downloadRunPdf(runId) {
      try {
        const response = await apiClient.get(`/billing/runs/${runId}/download`, {
          responseType: 'blob'
        });
        const contentType = response.headers['content-type'] || '';
        if (!contentType.includes('application/pdf')) {
          throw new Error('Antwort ist keine PDF-Datei');
        }
        const blob = new Blob([response.data], { type: 'application/pdf' });
        const url = window.URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = `abrechnung-${runId}.pdf`;
        document.body.appendChild(link);
        link.click();
        link.remove();
        window.URL.revokeObjectURL(url);
        return true;
      } catch (error) {
        this.feedback = { message: this.$t('billing.downloadError'), type: 'error' };
        return false;
      }
    },
    async deleteRun(run) {
      if (!run?.id || !this.isDeletable(run)) return;
      const confirmed = window.confirm(this.$t('billing.deleteConfirm'));
      if (!confirmed) return;
      try {
        await apiClient.delete(`/billing/runs/${run.id}`);
        await this.loadRuns();
        this.feedback = { message: this.$t('billing.deleteSuccess'), type: 'success' };
      } catch (error) {
        this.feedback = { message: this.$t('billing.deleteError'), type: 'error' };
      }
    },
    formatCurrency(value) {
      const parsed = Number.parseFloat(value ?? 0);
      return `${parsed.toFixed(2)} EUR`;
    },
    formatPeriod(start, end) {
      return `${start || '-'} - ${end || '-'}`;
    }
  }
};
</script>

<style scoped>
.billing-view {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.billing-subtitle {
  margin: 0.35rem 0 0;
  color: var(--text-light);
}

.billing-section {
  padding: 1rem;
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0.75rem;
  align-items: start;
}

.form-field {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.field-options {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.form-grid label {
  display: block;
  font-size: 0.85rem;
  margin-bottom: 0.2rem;
}

.omit-check {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.2rem;
  font-size: 0.8rem;
  color: var(--text-light);
}

.same-account-field {
  justify-content: flex-end;
}

.form-grid input,
.form-grid select {
  width: 100%;
}

.actions {
  margin-top: 0.9rem;
}

.billing-hint {
  margin: 0.75rem 0 0.5rem;
  color: var(--text-light);
}

.sessions-table {
  margin-top: 0.35rem;
}

.mapping-box {
  margin-top: 1rem;
  border-top: 1px dashed var(--border-color);
  padding-top: 0.8rem;
}

.mapping-grid {
  display: grid;
  gap: 0.45rem;
}

.mapping-row {
  display: grid;
  grid-template-columns: minmax(140px, 1fr) 90px 90px 90px;
  gap: 0.45rem;
  align-items: center;
}

.mapping-row label {
  margin: 0;
}

.mapping-editor-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  z-index: 4000;
  display: flex;
  justify-content: center;
  align-items: center;
}

.mapping-editor-panel {
  width: min(1200px, 95vw);
  max-height: 92vh;
  overflow: auto;
  background: #fff;
  border-radius: 10px;
  padding: 1rem;
}

.mapping-editor-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
}

.mapping-editor-controls {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  margin-bottom: 0.6rem;
  flex-wrap: wrap;
}

.nudge-controls {
  display: inline-flex;
  gap: 0.25rem;
}

.nudge-btn {
  min-width: 44px;
}

.mapping-canvas-wrap {
  position: relative;
  display: inline-block;
  border: 1px solid var(--border-color);
}

.mapping-canvas {
  display: block;
}

.mapping-markers {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.mapping-marker {
  position: absolute;
  width: 8px;
  height: 8px;
  background: #2563eb;
  border-radius: 50%;
  transform: translate(-50%, -50%);
}

.mapping-marker.active {
  width: 12px;
  height: 12px;
  background: #dc2626;
}

.billing-table {
  width: 100%;
  border-collapse: collapse;
}

.billing-table th,
.billing-table td {
  text-align: left;
  padding: 0.45rem;
  border-bottom: 1px solid var(--border-color);
}

.step-label {
  margin: 0 0 0.35rem;
  color: var(--text-light);
  font-size: 0.85rem;
}

.row-actions {
  display: flex;
  gap: 0.5rem;
}

.btn-danger {
  background: #b63737;
  color: #fff;
  border: none;
  border-radius: var(--border-radius-small);
  padding: 0.4rem 0.65rem;
}

.btn-danger:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.billing-feedback {
  margin: 0;
  padding: 0.6rem 0.8rem;
  border-radius: 8px;
  font-size: 0.92rem;
}

.billing-feedback.is-success {
  background: #e9f8ef;
  color: #1d6b3a;
}

.billing-feedback.is-error {
  background: #fdecec;
  color: #9f2c2c;
}
</style>
