diff --git a/gen_pages.py b/gen_pages.py index acf41c1..574899f 100644 --- a/gen_pages.py +++ b/gen_pages.py @@ -1,9 +1,26 @@ #!/usr/bin/env python3 """Renderuje karty HTML konceptów bundle'a do katalogu pages/. -Graf (viz.html) pokazuje strukturę; karty pokazują treść — ceny, dostępność, -plusy/minusy, listy do zrobienia. Węzeł w grafie linkuje do karty przez pole -`resource` (patrz gen_viz.py). +Wejściem dla człowieka jest `pages/start.html` (landing decyzyjny), a nie graf: +graf to załącznik pokazujący strukturę powiązań. Karty pokazują treść — ceny, +dostępność, plusy/minusy, listy do zrobienia. Węzeł w grafie linkuje do karty +przez pole `resource` (patrz gen_viz.py). + +Poza kartą na koncept skrypt generuje trzy strony zbiorcze: + + pages/index.html spis wszystkich kart, pogrupowany katalogami + pages/porownanie.html jedna tabela WSZYSTKICH ofert (nie regionów), + sortowalna klikiem, złożona z samych frontmatterów; + na górze podsumowanie per region z `wyjazd.md` + pages/start.html landing: trzy kafle (porównanie / analiza / graf) + plus linki do kart czterech regionów + +`start.html` jest jedyną stroną z linkami root-absolutnymi (`/viz.html`, +`/pages/...`), bo ten sam plik jest serwowany pod dwiema ścieżkami: jako +`/index.html` (korzeń serwisu) i jako `/pages/start.html`. Ścieżki względne +działałyby tylko pod jedną z nich. Wymaga to serwowania bundle'a z korzenia +domeny — lokalnie `python3 -m http.server` w katalogu bundle'a, na PIHA +korzeń wolumenu (komenda deployu na końcu wypisu skryptu). Renderer markdown: używa `python-markdown`, jeśli jest zainstalowany (rozszerzenia tables + fenced_code + sane_lists); w przeciwnym razie własnego @@ -358,11 +375,139 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } +""" + +SORT_JS = """ +document.querySelectorAll('table.sortable').forEach(function (table) { + var head = table.tHead.rows[0]; + Array.prototype.forEach.call(head.cells, function (th, i) { + th.tabIndex = 0; + th.setAttribute('role', 'button'); + th.addEventListener('click', function () { sortTable(table, i, th); }); + th.addEventListener('keydown', function (e) { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); sortTable(table, i, th); } + }); + }); +}); + +function sortTable(table, index, th) { + var dir = th.dataset.dir === 'asc' ? 'desc' : 'asc'; + Array.prototype.forEach.call(table.tHead.rows[0].cells, function (h) { + delete h.dataset.dir; + h.setAttribute('aria-sort', 'none'); + }); + th.dataset.dir = dir; + th.setAttribute('aria-sort', dir === 'asc' ? 'ascending' : 'descending'); + var body = table.tBodies[0]; + var rows = Array.prototype.slice.call(body.rows); + var mul = dir === 'asc' ? 1 : -1; + rows.sort(function (a, b) { + var x = a.cells[index], y = b.cells[index]; + var nx = x.dataset.num, ny = y.dataset.num; + if (nx !== undefined && ny !== undefined) return (parseFloat(nx) - parseFloat(ny)) * mul; + // Wiersze bez wartości liczbowej zawsze na końcu — w obie strony. + if (nx !== undefined) return -1; + if (ny !== undefined) return 1; + var kx = x.dataset.key !== undefined ? x.dataset.key : x.textContent; + var ky = y.dataset.key !== undefined ? y.dataset.key : y.textContent; + if (!kx) return 1; + if (!ky) return -1; + return kx.localeCompare(ky, 'pl') * mul; + }); + rows.forEach(function (row) { body.appendChild(row); }); +} """ -def _page_shell(title: str, depth: int, content: str) -> str: +def _page_shell( + title: str, + depth: int, + content: str, + *, + body_class: str = "", + nav: str | None = None, + script: str = "", +) -> str: up = "../" * depth + if nav is None: + nav = ( + f'← Start · ' + f'Wszystkie karty · ' + f'Porównanie ofert · ' + f'Graf' + ) + css_class = f' class="{body_class}"' if body_class else "" + script_tag = f"\n" if script else "" return f""" @@ -371,11 +516,11 @@ def _page_shell(title: str, depth: int, content: str) -> str: {html.escape(title)} — {html.escape(BUNDLE_NAME)} - - + + {content} +tych plików, edytuj .md i przegeneruj.{script_tag} """ @@ -434,12 +579,20 @@ def render_page(concept: Concept, pages: dict[str, str]) -> str: description = str(frontmatter.get("description") or "") lead = f'

{html.escape(description)}

' if description else "" + images = offer_images(concept) if is_offer(concept) else [] + gallery = render_gallery(images, str(frontmatter.get("nazwa") or concept.title)) + content = ( f'{chip}\n' - f"

{headline}

\n{lead}\n" + f"

{headline}

\n{lead}\n{gallery}\n" f"{_meta_table(frontmatter)}\n{rendered}" ) - return _page_shell(concept.title, concept.id.count("/"), content) + return _page_shell( + concept.title, + concept.id.count("/"), + content, + script=GALLERY_JS if len(images) > 1 else "", + ) _INDEX_HEADING_RE = re.compile(r"^#\s+(.+)$", re.M) @@ -505,6 +658,467 @@ def render_index(concepts: list[Concept]) -> str: return _page_shell("Karty", 0, content) +# --- zdjęcia obiektów (pages/img//, patrz gen_images.py) ----------- + +IMG_DIRNAME = "img" +IMG_DIR = PAGES_DIR / IMG_DIRNAME +IMG_EXTS = (".jpg", ".jpeg", ".png", ".webp") + + +def offer_image_paths(concept: Concept) -> list[str]: + """Zdjęcia oferty jako ścieżki względne wobec katalogu `pages/`. + + Mapowanie id → zdjęcia jest konwencją katalogową (pages/img//NN.jpg), + nie polem frontmattera — pliki .md zostają nietknięte. + """ + offer_id = str(concept.frontmatter.get("id") or "").strip() + if not offer_id: + return [] + directory = IMG_DIR / offer_id + if not directory.is_dir(): + return [] + files = sorted(p for p in directory.iterdir() if p.suffix.lower() in IMG_EXTS) + return [f"{IMG_DIRNAME}/{offer_id}/{p.name}" for p in files] + + +def offer_images(concept: Concept) -> list[str]: + """To samo, ale względem katalogu karty konceptu.""" + up = "../" * concept.id.count("/") + return [up + path for path in offer_image_paths(concept)] + + +def initials(name: str) -> str: + """Inicjały obiektu — placeholder dla ofert bez zdjęć.""" + words = [w for w in re.split(r"[\s/–—-]+", name) if w and w[0].isalnum()] + return "".join(w[0].upper() for w in words[:2]) or "?" + + +GALLERY_JS = """ +document.querySelectorAll('.gallery').forEach(function (gallery) { + var big = gallery.querySelector('.gallery-main img'); + gallery.querySelectorAll('.gallery-thumbs button').forEach(function (button) { + button.addEventListener('click', function () { + var img = button.querySelector('img'); + big.src = img.dataset.full || img.src; + gallery.querySelectorAll('.gallery-thumbs button').forEach(function (b) { + b.removeAttribute('aria-current'); + }); + button.setAttribute('aria-current', 'true'); + }); + }); +}); +""" + + +def render_gallery(images: list[str], alt: str) -> str: + if not images: + return "" + escaped_alt = html.escape(alt, quote=True) + first = html.escape(images[0], quote=True) + thumbs = "" + if len(images) > 1: + buttons = "".join( + f'' + for n, src in enumerate(images) + ) + thumbs = f'' + return ( + '" + ) + + +# --- podsumowanie per region (z wyjazd.md) ---------------------------- + +TRIP_FILE = "wyjazd.md" +BUDGET_ROW = "budżet grupy razem" +FALLBACK_ROW = "charakter" + +_TABLE_LINK_RE = re.compile(r"\[([^\]]+)\]\(/([^)\s]+\.md)\)") +_EMPHASIS_RE = re.compile(r"\*\*|\*|`") + + +@dataclass +class Region: + concept_id: str # np. "ischgl/region" + dirname: str # np. "ischgl" ("" dla Saalbach z korzenia) + label: str # np. "Ischgl/Paznaun" + budget: str = "" + conclusion: str = "" + + +def _plain(cell: str) -> str: + """Komórka tabeli markdown → czysty tekst (linki na same etykiety).""" + text = _LINK_RE.sub(r"\1", cell) + return _EMPHASIS_RE.sub("", text).strip() + + +def parse_regions() -> list[Region]: + """Regiony + budżet całkowity + jeden wniosek — z tabeli w `wyjazd.md`. + + Nic nie jest tu zaszyte na sztywno: kolumny bierzemy z nagłówka tabeli + porównawczej (te, które linkują do `*/region.md`), budżet z wiersza + „Budżet grupy razem”, a wniosek z pierwszej pozycji „Wniosków + przekrojowych”, która dany region wymienia. Dorzucenie piątego wariantu + do `wyjazd.md` wystarczy, żeby pojawił się na stronach. + """ + path = BUNDLE_ROOT / TRIP_FILE + if not path.is_file(): + return [] + text = path.read_text(encoding="utf-8") + + regions: list[Region] = [] + rows: dict[str, list[str]] = {} + lines = text.split("\n") + for n, line in enumerate(lines): + if not line.strip().startswith("|"): + continue + header = _cells(line) + targets = [_TABLE_LINK_RE.search(c) for c in header[1:]] + if sum(1 for t in targets if t and t.group(2).endswith("region.md")) < 2: + continue + if n + 1 >= len(lines) or not _TABLE_SEP_RE.match(lines[n + 1]): + continue + for match in targets: + if not match or not match.group(2).endswith("region.md"): + continue + concept_id = match.group(2)[: -len(".md")] + regions.append( + Region( + concept_id=concept_id, + dirname=posixpath.dirname(concept_id), + label=_plain(match.group(1)), + ) + ) + for row in lines[n + 2 :]: + if not row.strip().startswith("|"): + break + cells = _cells(row) + rows[_plain(cells[0]).lower()] = [_plain(c) for c in cells[1:]] + break + + conclusions = parse_conclusions(text) + for index, region in enumerate(regions): + for key in (BUDGET_ROW, FALLBACK_ROW): + values = rows.get(key) or [] + if index < len(values) and values[index]: + if key == BUDGET_ROW: + region.budget = values[index] + elif not region.conclusion: + region.conclusion = values[index] + keyword = region.dirname or region.label.split()[0] + for raw in conclusions: + if keyword.lower() in raw.lower(): + region.conclusion = _plain(raw.split(". ")[0]).rstrip(".") + "." + break + return regions + + +_ORDERED_ITEM_RE = re.compile(r"^\s*\d+[.)]\s+(.*)$") + + +def parse_conclusions(text: str) -> list[str]: + """Pozycje listy numerowanej z sekcji „Wnioski przekrojowe”.""" + section = re.split(r"^#{2,4}\s+Wnioski przekrojowe\s*$", text, flags=re.M) + if len(section) < 2: + return [] + items: list[str] = [] + for line in section[1].split("\n"): + if line.startswith("#"): + break + match = _ORDERED_ITEM_RE.match(line) + if match: + items.append(match.group(1).strip()) + elif line.startswith((" ", "\t")) and line.strip() and items: + items[-1] += " " + line.strip() + return items + + +# --- karta porównania ofert ------------------------------------------- + +# (nagłówek, pole frontmattera / klucz specjalny, czy sortować liczbowo) +COLUMNS: list[tuple[str, str, bool]] = [ + ("Oferta", "_nazwa", False), + ("Priorytet", "priorytet", False), + ("Region", "_region", False), + ("Miejscowość", "miejscowosc", False), + ("€ / apart. / tydzień", "cena_za_apartament_tydzien", True), + ("Rodzina 2+2", "koszt_rodzina_2p2", True), + ("Rodzina 2+1", "koszt_rodzina_2p1", True), + ("Status ceny", "status_ceny", False), + ("Od wyciągu", "odleglosc_od_wyciagu", False), + ("WWW", "www", False), +] + +_DIGIT_GAP_RE = re.compile(r"(?<=\d)[\s ](?=\d)") +_NUMBER_RE = re.compile(r"\d+(?:[.,]\d+)?") + + +def sort_number(value: str) -> float | None: + """Pierwsza liczba w tekście, ze spacją jako separatorem tysięcy.""" + match = _NUMBER_RE.search(_DIGIT_GAP_RE.sub("", value)) + return float(match.group(0).replace(",", ".")) if match else None + + +def is_offer(concept: Concept) -> bool: + return str(concept.frontmatter.get("type", "")).startswith("oferta") + + +def _shorten(value: str) -> str: + """Sam rdzeń wartości; doprecyzowanie w nawiasie/po myślniku idzie do title. + + „SZACUNEK — silnik online na alpenparks.at” w komórce rozpycha tabelę na + trzy wiersze, a i tak koduje ją kolor wiersza. Pełna treść zostaje pod + kursorem, nic nie ginie. + """ + for separator in (" — ", " (", " - ", " dla "): + head = value.split(separator)[0].strip() + if head and head != value: + return head + return value + + +# Kolumny, w których doprecyzowanie JEST treścią — skracanie zabrałoby sens. +NO_SHORTEN = {"odleglosc_od_wyciagu"} + + +def _cell(value: str, numeric: bool, suffix: str = "", short: bool = True) -> str: + attrs = "" + if not value: + return '—' + if numeric: + number = sort_number(value) + if number is not None: + attrs += f' data-num="{number:g}"' + else: + attrs += f' data-key="{html.escape(value, quote=True)}"' + shown = _shorten(value) if short else value + if shown != value: + attrs += f' title="{html.escape(value, quote=True)}"' + return f"{html.escape(shown + suffix)}" + + +def render_comparison(concepts: list[Concept], regions: list[Region]) -> str: + """Jedna tabela wszystkich ofert bundle'a, prosto z frontmatterów.""" + by_dir = {r.dirname: r for r in regions} + offers = [c for c in concepts if is_offer(c)] + # Domyślna kolejność: najtańsza rodzina 2+2 na górze — pierwsze pytanie, + # które grupa zadaje. Resztę porządków daje sortowanie klikiem. + offers.sort( + key=lambda c: ( + sort_number(str(c.frontmatter.get("koszt_rodzina_2p2") or "")) or 1e9, + c.title, + ) + ) + + rows = [] + for concept in offers: + frontmatter = concept.frontmatter + status = str(frontmatter.get("status_ceny") or "") + upper = status.upper() + css_row = ( + "verified" if "ZWERYFIKOWANE" in upper + else "estimate" if "SZACUNEK" in upper + else "" + ) + dirname = posixpath.dirname(posixpath.dirname(concept.id)) + region = by_dir.get(dirname) + region_label = region.label if region else (dirname or "—") + href = html.escape(posixpath.relpath(concept.page, "."), quote=True) + + images = offer_image_paths(concept) # porownanie.html leży w pages/ + name = str(frontmatter.get("nazwa") or concept.title) + if images: + thumb = ( + f'' + ) + else: + thumb = ( + f'' + f"{html.escape(initials(name))}" + ) + + cells = [f'{thumb}'] + for _, field, numeric in COLUMNS: + if field == "_nazwa": + cells.append( + f'' + f'{html.escape(name)}' + ) + elif field == "_region": + page = html.escape(f"{region.concept_id}.html", quote=True) if region else "" + label = html.escape(region_label) + shown = f'{label}' if region else label + cells.append( + f'{shown}' + ) + elif field == "www": + url = str(frontmatter.get("www") or "").strip() + if url: + host = re.sub(r"^https?://(www\.)?", "", url).split("/")[0] + cells.append( + f'' + f'{html.escape(host)}' + ) + else: + cells.append('—') + else: + value = str(frontmatter.get(field) or "") + suffix = "" + if not value and field == "cena_za_apartament_tydzien": + # Dom dla całej grupy wyceniany jest w całości — pokazujemy + # to zamiast pustej komórki, ale z etykietą „za co”. + value = str(frontmatter.get("cena_za_calosc_tydzien") or "") + suffix = " (cały dom)" if value else "" + cells.append( + _cell(value, numeric, suffix, short=field not in NO_SHORTEN) + ) + klass = f' class="{css_row}"' if css_row else "" + rows.append(f"{''.join(cells)}") + + head = 'Zdjęcie' + "".join( + f'{html.escape(title)}' + for title, _, _ in COLUMNS + ) + table = ( + '
' + f"{head}{''.join(rows)}
" + ) + + summary = "" + if regions: + items = [] + for region in regions: + budget = ( + f'Budżet grupy: ' + f"{html.escape(region.budget)}" + if region.budget + else "" + ) + conclusion = ( + f"

{render_inline(region.conclusion)}

" if region.conclusion else "" + ) + items.append( + f'
  • ' + f"{html.escape(region.label)}
    {budget}{conclusion}
  • " + ) + summary = ( + "

    Regiony w skrócie

    \n" + '
      ' + "".join(items) + "
    " + ) + + content = ( + "

    Porównanie ofert

    \n" + f'

    Wszystkie {len(offers)} rozważanych noclegów w jednej ' + "tabeli — dane prosto z frontmatterów kart. Kliknij nagłówek, żeby " + "posortować; domyślnie od najtańszej rodziny 2+2.

    \n" + f"{summary}\n" + "

    Wszystkie oferty

    \n" + f"{table}\n" + '

    cena zweryfikowana ' + '  szacunek do potwierdzenia

    ' + ) + return _page_shell( + "Porównanie ofert", 0, content, body_class="wide", script=SORT_JS + ) + + +# --- landing decyzyjny ------------------------------------------------ + +# start.html jest serwowany i jako /index.html, i jako /pages/start.html — +# tylko ścieżki root-absolutne działają pod obiema (patrz docstring modułu). +TILES = [ + ("/pages/porownanie.html", "Porównanie ofert", + "Wszystkie noclegi w jednej sortowalnej tabeli: ceny, status ceny, " + "priorytet, odległość od wyciągu."), + ("/pages/wyjazd.html", "Analiza wariantów", + "Cztery regiony obok siebie: skipassy, dojazd, budżet całkowity grupy " + "i wnioski przekrojowe."), + ("/viz.html", "Graf powiązań", + "Struktura bazy wiedzy — koncepty, linki między nimi i adresy " + "zewnętrzne. Załącznik, nie punkt wejścia."), +] + + +def render_start(concepts: list[Concept], regions: list[Region]) -> str: + tiles = "".join( + f'
  • {html.escape(title)}' + f"{html.escape(description)}
  • " + for href, title, description in TILES + ) + + by_id = {c.id: c for c in concepts} + cards = [] + for region in regions: + concept = by_id.get(region.concept_id) + description = region.budget or ( + str(concept.frontmatter.get("description") or "") if concept else "" + ) + style = "" + hero = region_hero(concepts, region) + if hero: + style = ( + ' class="hero" style="background-image:linear-gradient(' + "rgba(15,23,42,.62),rgba(15,23,42,.72)),url('" + f"/pages/{html.escape(hero, quote=True)}')\"" + ) + label = html.escape(region.label) + budget = ( + f"Budżet grupy: {html.escape(region.budget)}" + if region.budget + else f"{html.escape(description)}" + ) + cards.append( + f'
  • {label}{budget}
  • " + ) + + content = ( + f"

    {html.escape(BUNDLE_NAME)}

    \n" + '

    16–23.01.2027, 4 rodziny / 14 osób, wyjazd z Warszawy. ' + "Od czego zacząć:

    \n" + f'
      {tiles}
    \n' + "

    Regiony

    \n" + '

    Karta regionu = skipassy, dojazd, terminy, koszty ' + "życia i budżet całkowity.

    \n" + f'
      {"".join(cards)}
    ' + ) + nav = ( + 'Wszystkie karty · ' + 'Porównanie ofert · ' + 'Graf' + ) + return _page_shell("Start", 0, content, nav=nav) + + +def region_hero(concepts: list[Concept], region: Region) -> str: + """Pierwsze zdjęcie najlepszej oferty regionu (priorytet PLAN A).""" + offers = [ + c + for c in concepts + if is_offer(c) and posixpath.dirname(posixpath.dirname(c.id)) == region.dirname + ] + + def rank(concept: Concept) -> tuple[int, str]: + priority = str(concept.frontmatter.get("priorytet") or "").upper() + for n, marker in enumerate(("PLAN A", "PLAN B", "PLAN C")): + if priority.startswith(marker): + return (n, concept.id) + return (9, concept.id) + + for concept in sorted(offers, key=rank): + images = offer_image_paths(concept) + if images: + return images[0] + return "" + + # --- raport braków ---------------------------------------------------- @@ -535,13 +1149,27 @@ def missing_data(concepts: list[Concept]) -> list[tuple[str, list[str]]]: # --- main ------------------------------------------------------------- +DEPLOY_COMMAND = """ssh piha 'rm -rf /tmp/narty27-deploy && mkdir -p /tmp/narty27-deploy' \\ + && scp -r viz.html pages piha:/tmp/narty27-deploy/ \\ + && ssh piha 'docker run --rm -v narty27_narty27_content:/content \\ + -v /tmp/narty27-deploy:/src:ro alpine sh -c \\ + "rm -rf /content/pages && cp -r /src/viz.html /src/pages /content/ \\ + && cp /src/pages/start.html /content/index.html && ls -la /content"' \\ + && ssh piha 'rm -rf /tmp/narty27-deploy'""" + + def main() -> int: concepts = load_concepts() pages = {c.id: c.page for c in concepts} + regions = parse_regions() - if PAGES_DIR.exists(): - shutil.rmtree(PAGES_DIR) - PAGES_DIR.mkdir(parents=True) + # Katalog img/ jest wejściem, nie wyjściem tego skryptu (wypełnia go + # gen_images.py) — czyścimy wszystko poza nim. + PAGES_DIR.mkdir(parents=True, exist_ok=True) + for entry in PAGES_DIR.iterdir(): + if entry.name == IMG_DIRNAME: + continue + shutil.rmtree(entry) if entry.is_dir() else entry.unlink() written = [] for concept in concepts: @@ -550,9 +1178,14 @@ def main() -> int: out.write_text(render_page(concept, pages), encoding="utf-8") written.append(out) - index = PAGES_DIR / "index.html" - index.write_text(render_index(concepts), encoding="utf-8") - written.append(index) + for name, markup in ( + ("index.html", render_index(concepts)), + ("porownanie.html", render_comparison(concepts, regions)), + ("start.html", render_start(concepts, regions)), + ): + path = PAGES_DIR / name + path.write_text(markup, encoding="utf-8") + written.append(path) print(f"Renderer markdown: {RENDERER}") print(f"Zapisano {len(written)} plików w {PAGES_DIR}:") @@ -560,6 +1193,18 @@ def main() -> int: size = path.stat().st_size / 1024 print(f" {path.relative_to(BUNDLE_ROOT)} ({size:.1f} KiB)") + print() + offers = [c for c in concepts if is_offer(c)] + with_photos = [c for c in offers if offer_image_paths(c)] + print( + f"Regiony z wyjazd.md: {len(regions)} " + f"({', '.join(r.label for r in regions) or '—'})" + ) + print( + f"Oferty w tabeli porównawczej: {len(offers)}, " + f"ze zdjęciami: {len(with_photos)} (zdjęcia dokłada gen_images.py)" + ) + gaps = missing_data(concepts) print() if gaps: @@ -570,6 +1215,10 @@ def main() -> int: print(f" – {item}") else: print("Braków w danych ofert nie wykryto.") + + print() + print("Deploy na PIHA (korzeń serwisu = start.html, graf pod /viz.html):") + print(DEPLOY_COMMAND) return 0 diff --git a/pages/index.html b/pages/index.html index 41e6796..5de6ea4 100644 --- a/pages/index.html +++ b/pages/index.html @@ -57,10 +57,80 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - +

    Ski 2027 — Skicircus Saalbach

    Karty konceptów bundle'a — pełna treść każdego dokumentu (ceny, dostępność, plusy/minusy). Struktura powiązań: graf.

    Wyjazd i wiedza ogólna

    diff --git a/pages/ischgl/oferty/apartment-brandau-kappl.html b/pages/ischgl/oferty/apartment-brandau-kappl.html index 2ac0267..400ab81 100644 --- a/pages/ischgl/oferty/apartment-brandau-kappl.html +++ b/pages/ischgl/oferty/apartment-brandau-kappl.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + oferta

    Apartment Brandau (Kappl) — PLAN A

    Rodzinny dom z sześcioma apartamentami w Kappl — bierzemy cztery, ceny doliny zamiast cen Ischgl, PLAN A wariantu Ischgl.

    +
    Metadane (frontmatter)
    typeoferta
    titleApartment Brandau (Kappl)
    descriptionRodzinny dom z sześcioma apartamentami w Kappl — bierzemy cztery, ceny doliny zamiast cen Ischgl, PLAN A wariantu Ischgl.
    timestamp2026-08-01T00:00:00Z
    tagsnocleg, ischgl, kappl, plan-a
    idapartment-brandau-kappl
    nazwaApartment Brandau
    operatorprywatna rodzina
    miejscowoscKappl (Paznaun)
    odleglosc_od_wyciaguskibus (przystanek blisko domu) → Kappl / Ischgl ~10 min
    uklad_dla_grupy6 apartamentów w jednym domu → 4 dla nas
    cena_za_apartament_tydzien~900–1 300 € (szacunek, styczeń)
    koszt_rodzina_2p2~900–1 300 €
    koszt_rodzina_2p1~750–1 100 €
    status_cenySZACUNEK — wycena mailowo
    priorytetPLAN A dla wariantu Ischgl
    wwwhttp://www.kappl-apartment-brandau.at
    ostatnia_aktualizacja2026-08-01

    W skrócie

    @@ -76,5 +147,20 @@ footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line);
    • Mail (szablon EN): 4 apartamenty, 16–23.01.2027, konfiguracja 4+4+3+3, cena grupowa.
    • Alternatywy w Kappl, jeśli brak miejsc: Apartments Kappl (apartments.kappl.at — skibus 100 m, narciarnia z suszarką), Ferienwohnen Mattle (centrum, blisko trasy i skibusa), Apart Avenzio (3 min od gondoli Kappl).
    Wygenerowane z bundle'a OKF przez gen_pages.py — nie edytuj tych plików, edytuj .md i przegeneruj.
    + diff --git a/pages/ischgl/oferty/apartments-kappl.html b/pages/ischgl/oferty/apartments-kappl.html index 11a76d8..de7c260 100644 --- a/pages/ischgl/oferty/apartments-kappl.html +++ b/pages/ischgl/oferty/apartments-kappl.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + oferta

    Apartments Kappl — PLAN B

    Tyrolski dom apartamentowy 100 m od przystanku skibusa bez przesiadki do Ischgl — liczba wolnych jednostek do potwierdzenia, PLAN B wariantu Ischgl.

    +
    Metadane (frontmatter)
    typeoferta
    titleApartments Kappl
    descriptionTyrolski dom apartamentowy 100 m od przystanku skibusa bez przesiadki do Ischgl — liczba wolnych jednostek do potwierdzenia, PLAN B wariantu Ischgl.
    timestamp2026-08-01T00:00:00Z
    tagsnocleg, ischgl, kappl, plan-b
    idapartments-kappl
    nazwaApartments Kappl
    operatorprywatny
    miejscowoscKappl (Paznaun)
    odleglosc_od_wyciaguprzystanek skibusa 100 m — bez przesiadki do Ischgl, Galtür, See
    uklad_dla_grupykilka apartamentów w jednym domu
    cena_za_apartament_tydzien~900–1 400 € (szacunek, styczeń)
    koszt_rodzina_2p2~900–1 400 €
    koszt_rodzina_2p1~750–1 150 €
    status_cenySZACUNEK — wycena mailowo
    priorytetPLAN B dla wariantu Ischgl
    wwwhttps://www.apartments.kappl.at
    ostatnia_aktualizacja2026-08-01

    Fakty

    @@ -74,5 +145,20 @@ footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line);
    • Mail: ile apartamentów dostępnych 16–23.01.2027, konfiguracje łóżek.
    • Jeśli < 4 jednostek: dobrać drugi dom w Kappl (lista alternatyw w karcie Brandau).
    Wygenerowane z bundle'a OKF przez gen_pages.py — nie edytuj tych plików, edytuj .md i przegeneruj.
    + diff --git a/pages/ischgl/region.html b/pages/ischgl/region.html index 5eaf9f2..2fe8a64 100644 --- a/pages/ischgl/region.html +++ b/pages/ischgl/region.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + region

    Ischgl / Paznaun — informacje o regionie

    Wariant alternatywny: 239 km Silvretta Areny z jazdą do bezcłowego Samnaun, nocleg w tańszym Kappl — skipass zweryfikowany na 26/27, bez zniżki dla 17-latków.

    +
    Metadane (frontmatter)
    typeregion
    titleIschgl / Silvretta Arena + dolina Paznaun
    descriptionWariant alternatywny: 239 km Silvretta Areny z jazdą do bezcłowego Samnaun, nocleg w tańszym Kappl — skipass zweryfikowany na 26/27, bez zniżki dla 17-latków.
    timestamp2026-08-01T00:00:00Z
    tagsregion, ischgl, wariant, skipass, logistyka
    idischgl-paznaun
    nazwaIschgl / Silvretta Arena + dolina Paznaun
    krajAustria
    regionTyrol
    sezon2026/2027
    grupa4 rodziny (2×2+2, 2×2+1) = 14 osób
    wyjazd_zWarszawa
    rekomendowany_termin16–23.01.2027 (alt. 9–16.01)
    strategia_budzetowamieszkać w Kappl, nie w Ischgl
    ostatnia_aktualizacja2026-08-01

    Wariant "Ischgl, ale w budżecie": jeździmy w Silvretta Arenie, mieszkamy w Kappl (10 min od Ischgl, skibus gratis). Nocleg w samym Ischgl odrzucony cenowo już na etapie pierwszego porównania.

    diff --git a/pages/kronplatz/oferty/haeuslerhof-valdaora.html b/pages/kronplatz/oferty/haeuslerhof-valdaora.html index 21bd5d0..0f5e867 100644 --- a/pages/kronplatz/oferty/haeuslerhof-valdaora.html +++ b/pages/kronplatz/oferty/haeuslerhof-valdaora.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + oferta

    Häuslerhof (Valdaora) — PLAN A

    Gospodarstwo z dokładnie czterema apartamentami — grupa zajmuje cały obiekt, anulacja bezpłatna do 2 miesięcy przed, PLAN A wariantu Kronplatz.

    +
    Metadane (frontmatter)
    typeoferta
    titleHäuslerhof (Valdaora)
    descriptionGospodarstwo z dokładnie czterema apartamentami — grupa zajmuje cały obiekt, anulacja bezpłatna do 2 miesięcy przed, PLAN A wariantu Kronplatz.
    timestamp2026-08-01T00:00:00Z
    tagsnocleg, kronplatz, valdaora, plan-a
    idhaeuslerhof-valdaora
    nazwaHäuslerhof
    operatorgospodarstwo rodzinne (agroturystyka premium)
    miejscowoscValdaora/Olang
    odleglosc_od_wyciaguskibus do gondoli Olang (Kronplatz)
    uklad_dla_grupyDOKŁADNIE 4 apartamenty (2, 4 lub 6 os.) — cały obiekt dla nas
    cena_za_apartament_tydzien~800–1 300 € (szacunek, styczeń)
    koszt_rodzina_2p2~900–1 300 €
    koszt_rodzina_2p1~800–1 100 €
    status_cenySZACUNEK — wycena mailowo
    priorytetPLAN A dla wariantu Kronplatz
    anulacjabezpłatnie do 2 mies. przed; 2 mies.–4 tyg.: 50%; <4 tyg.: 80%
    wwwhttps://www.haeuslerhof.it
    ostatnia_aktualizacja2026-08-01

    W skrócie

    @@ -76,5 +147,20 @@ footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line);
    • Mail: wszystkie 4 apartamenty, 16–23.01.2027, 4+4+3+3.
    • Potwierdzić odległość do przystanku skibusa i częstotliwość rano.
    Wygenerowane z bundle'a OKF przez gen_pages.py — nie edytuj tych plików, edytuj .md i przegeneruj.
    + diff --git a/pages/kronplatz/oferty/residence-corones-valdaora.html b/pages/kronplatz/oferty/residence-corones-valdaora.html index 7b59d21..bfbe1c8 100644 --- a/pages/kronplatz/oferty/residence-corones-valdaora.html +++ b/pages/kronplatz/oferty/residence-corones-valdaora.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + oferta

    Residence Corones (Valdaora) — PLAN B

    Rezydencja w centrum Valdaory z przystankiem skibusa przed domem i Digital Guestpassem w cenie — PLAN B wariantu Kronplatz.

    +
    Metadane (frontmatter)
    typeoferta
    titleResidence Corones (Valdaora)
    descriptionRezydencja w centrum Valdaory z przystankiem skibusa przed domem i Digital Guestpassem w cenie — PLAN B wariantu Kronplatz.
    timestamp2026-08-01T00:00:00Z
    tagsnocleg, kronplatz, valdaora, plan-b
    idresidence-corones-valdaora
    nazwaResidence Corones
    operatorrodzina Pörnbacher-Oberhuber
    miejscowoscValdaora (centrum)
    odleglosc_od_wyciaguprzystanek skibusa PRZED domem → 5 min do gondoli Kronplatz
    uklad_dla_grupykilka apartamentów w rezydencji
    cena_za_apartament_tydzien~900–1 400 € (szacunek, styczeń)
    koszt_rodzina_2p2~900–1 400 €
    koszt_rodzina_2p1~800–1 150 €
    status_cenySZACUNEK — wycena mailowo (reception@corones.it)
    priorytetPLAN B dla wariantu Kronplatz
    wwwhttps://corones.it
    ostatnia_aktualizacja2026-08-01

    Fakty

    diff --git a/pages/kronplatz/region.html b/pages/kronplatz/region.html index 0e8f927..0ffd6be 100644 --- a/pages/kronplatz/region.html +++ b/pages/kronplatz/region.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + region

    Kronplatz — informacje o regionie

    Wariant alternatywny: 121 km na jednej górze plus cały Dolomiti Superski w karnecie, baza w tanim Olang/Valdaora, junior do 18 lat bez dopłaty.

    +
    Metadane (frontmatter)
    typeregion
    titleKronplatz / Plan de Corones (Południowy Tyrol)
    descriptionWariant alternatywny: 121 km na jednej górze plus cały Dolomiti Superski w karnecie, baza w tanim Olang/Valdaora, junior do 18 lat bez dopłaty.
    timestamp2026-08-01T00:00:00Z
    tagsregion, kronplatz, wariant, skipass, logistyka
    idkronplatz
    nazwaKronplatz / Plan de Corones (Południowy Tyrol)
    krajWłochy
    regionVal Pusteria / Pustertal
    sezon2026/2027
    grupa4 rodziny (2×2+2, 2×2+1) = 14 osób
    wyjazd_zWarszawa
    rekomendowany_termin16–23.01.2027 (alt. 9–16.01)
    strategia_budzetowabaza w Olang/Valdaora — taniej niż Val Gardena, skibus pod gondolę
    ostatnia_aktualizacja2026-08-01

    Wariant "Dolomity, ale nie Sella Ronda": ta sama jakość karnetu Dolomiti Superski i widoków, zupełnie inny teren. Baza: Olang/Valdaora u podnóża góry — wyraźnie taniej niż doliny Sella Rondy.

    diff --git a/pages/oferty/alpenparks-rehrenberg.html b/pages/oferty/alpenparks-rehrenberg.html index 3560d35..d7394a1 100644 --- a/pages/oferty/alpenparks-rehrenberg.html +++ b/pages/oferty/alpenparks-rehrenberg.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + oferta

    AlpenParks Rehrenberg Viehhofen — PLAN C

    Najtańszy nowy obiekt na liście — przestronne apartamenty z prywatnymi saunami w Viehhofen, wariant rezerwowy o mieszanych opiniach, PLAN C.

    +
    Metadane (frontmatter)
    typeoferta
    titleAlpenParks Apartments Rehrenberg
    descriptionNajtańszy nowy obiekt na liście — przestronne apartamenty z prywatnymi saunami w Viehhofen, wariant rezerwowy o mieszanych opiniach, PLAN C.
    timestamp2026-07-31T00:00:00Z
    tagsnocleg, viehhofen, plan-c
    idalpenparks-rehrenberg
    nazwaAlpenParks Apartments Rehrenberg
    operatorAlpenParks
    miejscowoscViehhofen
    gps47.3662, 12.7317
    odleglosc_od_wyciagu5–10 min pieszo do gondoli zellamseeXpress
    ocena4.2/5 (Google) — opinie mieszane
    uklad_dla_grupy4 osobne apartamenty
    cena_za_apartament_tydzien~1 100–1 500 € (szacunek, styczeń poza feriami)
    koszt_rodzina_2p2~1 100–1 500 €
    koszt_rodzina_2p1~950–1 250 €
    status_cenySZACUNEK — silnik online na alpenparks.at
    priorytetPLAN C (rezerwowy)
    wwwhttps://www.alpenparks.at
    ostatnia_aktualizacja2026-07-31

    W skrócie

    @@ -79,5 +150,20 @@ footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line);
    • Tylko jeśli plany A i B nie wypalą: sprawdzić ceny datami 16–23.01.2027.
    • Zweryfikować najnowsze opinie (Booking/Google, filtr: zima 2026).
    Wygenerowane z bundle'a OKF przez gen_pages.py — nie edytuj tych plików, edytuj .md i przegeneruj.
    + diff --git a/pages/oferty/alpenparks-sonnleiten.html b/pages/oferty/alpenparks-sonnleiten.html index 18f798f..7858476 100644 --- a/pages/oferty/alpenparks-sonnleiten.html +++ b/pages/oferty/alpenparks-sonnleiten.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + oferta

    AlpenParks Sonnleiten Saalbach

    Duży aparthotel ~2 min pieszo od stoku w Saalbach, z basenem i sauną w cenie — największa szansa na 4 wolne apartamenty w jednym tygodniu.

    +
    Metadane (frontmatter)
    typeoferta
    titleAlpenParks Hotel & Apartment Sonnleiten
    descriptionDuży aparthotel ~2 min pieszo od stoku w Saalbach, z basenem i sauną w cenie — największa szansa na 4 wolne apartamenty w jednym tygodniu.
    timestamp2026-07-31T00:00:00Z
    tagsnocleg, saalbach, aparthotel
    idalpenparks-sonnleiten
    nazwaAlpenParks Hotel & Apartment Sonnleiten
    operatorAlpenParks
    miejscowoscSaalbach
    gps47.3953, 12.6378
    odleglosc_od_wyciagu~2 min pieszo do stoku
    ocena4.6/5 (Google)
    uklad_dla_grupy4 osobne apartamenty (po 1 na rodzinę)
    cena_za_apartament_tydzien~1 400–1 900 € (szacunek, styczeń poza feriami)
    koszt_rodzina_2p2~1 400–1 900 €
    koszt_rodzina_2p1~1 200–1 500 €
    status_cenySZACUNEK — silnik rezerwacyjny online na alpenparks.at
    priorytetPLAN A-bis (największa szansa na 4 wolne apartamenty)
    wwwhttps://www.alpenparks.at
    ostatnia_aktualizacja2026-07-31

    W skrócie

    @@ -79,5 +150,20 @@ footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line);
    • Wpisać daty 16–23.01.2027 w silnik na alpenparks.at → zrzut cen rzeczywistych dla 4 jednostek.
    • Równolegle mail grupowy z pytaniem o rabat przy 4 apartamentach.
    • Sprawdzić politykę anulacji (sieciówki często mają elastyczne taryfy za dopłatą — przy grupie 14 os. warto).
    Wygenerowane z bundle'a OKF przez gen_pages.py — nie edytuj tych plików, edytuj .md i przegeneruj.
    + diff --git a/pages/oferty/dom-dla-grupy.html b/pages/oferty/dom-dla-grupy.html index 204e1a1..32a4274 100644 --- a/pages/oferty/dom-dla-grupy.html +++ b/pages/oferty/dom-dla-grupy.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + oferta-koncept

    Dom dla całej grupy (12–14 os.) — koncept

    Koncept wynajęcia jednego domu wakacyjnego dla całej grupy zamiast czterech osobnych apartamentów — klimat chaletowy w połowie ceny Glemm Lodge.

    +
    Metadane (frontmatter)
    typeoferta-koncept
    titleDuży dom wakacyjny 12–14 os. (do wyszukania)
    descriptionKoncept wynajęcia jednego domu wakacyjnego dla całej grupy zamiast czterech osobnych apartamentów — klimat chaletowy w połowie ceny Glemm Lodge.
    timestamp2026-07-31T00:00:00Z
    tagsnocleg, dom-wakacyjny, koncept
    iddom-dla-grupy
    nazwaDuży dom wakacyjny 12–14 os. (do wyszukania)
    miejscowoscViehhofen / Maishofen / strona Fieberbrunn
    uklad_dla_grupyjeden dom dla całej grupy
    cena_za_calosc_tydzien4 500–6 500 € (szacunek, styczeń poza feriami)
    koszt_rodzina_2p2~1 400–1 900 € (podział od osoby)
    koszt_rodzina_2p1~1 100–1 500 € (podział od osoby)
    status_cenySZACUNEK — konkretny dom do znalezienia
    priorytetWARIANT RÓWNOLEGŁY (klimat 'wszyscy razem')
    gdzie_szukaccasamundo.pl, interhome.pl, e-domizil.de, vrbo.com, belvilla.com, allchalets.com
    ostatnia_aktualizacja2026-07-31

    W skrócie

    diff --git a/pages/oferty/glemm-lodge.html b/pages/oferty/glemm-lodge.html index 15d4c44..1f5e5ae 100644 --- a/pages/oferty/glemm-lodge.html +++ b/pages/oferty/glemm-lodge.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + oferta

    Glemm Lodge Apartments — poza budżetem (dane referencyjne)

    Jedyna oferta z w pełni zweryfikowanym cennikiem (~4 370 €/rodzinę) — odrzucona budżetowo, służy jako punkt odniesienia cenowego.

    +
    Metadane (frontmatter)
    typeoferta
    titleGlemm Lodge Apartments ⭐⭐⭐⭐
    descriptionJedyna oferta z w pełni zweryfikowanym cennikiem (~4 370 €/rodzinę) — odrzucona budżetowo, służy jako punkt odniesienia cenowego.
    timestamp2026-07-31T00:00:00Z
    tagsnocleg, saalbach, referencja
    idglemm-lodge
    nazwaGlemm Lodge Apartments ⭐⭐⭐⭐
    operatorDieter Weschenfelder (prywatny)
    kontaktinfo@glemmlodge-apartments.com
    miejscowoscmiędzy Saalbach a Hinterglemm
    adresGlemmtaler Landesstr. 263, 5753 Saalbach
    gps47.3879, 12.6178
    odleglosc_od_wyciaguzjazd trasą 86 pod dom; skibus pod drzwiami (5 min do gondoli)
    ocena9.1/10 (Booking), 4.8/5 (Google)
    uklad_dla_grupy2 apartamenty po 7 os. (Top 2 + Top 3)
    cena_za_apartament_tydzien7 000 € + 500 € sprzątanie + taksy (CENNIK OFICJALNY 26/27)
    koszt_rodzina_2p2~4 370 €
    koszt_rodzina_2p1~3 277 €
    status_cenyZWERYFIKOWANE (cennik + kalendarz obiektu, stan 31.07.2026)
    priorytetODRZUCONA BUDŻETOWO (wraca do gry przy rabacie do ~5 000 €/apartament)
    wwwhttps://glemmlodge-apartments.com
    ostatnia_aktualizacja2026-07-31

    Dlaczego w bazie mimo odrzucenia

    @@ -84,5 +155,20 @@ footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line);
    • Mail do Dietera: cena za 7 os./apartament zamiast 10–12 — jeśli ≤ 5 000 €, wariant wraca do rozważań (~2 900/2 200 € na rodzinę).
    Wygenerowane z bundle'a OKF przez gen_pages.py — nie edytuj tych plików, edytuj .md i przegeneruj.
    + diff --git a/pages/oferty/kristall-schattbergxpress.html b/pages/oferty/kristall-schattbergxpress.html index aa49d1a..c42680c 100644 --- a/pages/oferty/kristall-schattbergxpress.html +++ b/pages/oferty/kristall-schattbergxpress.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + oferta

    Appartementhaus Kristall at SchattbergXpress — PLAN A

    Apartamenty 50 m od gondoli Schattberg X-press w centrum Saalbach — najlepsza lokalizacja w rozważanym budżecie, PLAN A.

    +
    Metadane (frontmatter)
    typeoferta
    titleAppartementhaus Kristall at SchattbergXpress
    descriptionApartamenty 50 m od gondoli Schattberg X-press w centrum Saalbach — najlepsza lokalizacja w rozważanym budżecie, PLAN A.
    timestamp2026-07-31T00:00:00Z
    tagsnocleg, saalbach, plan-a
    idkristall-schattbergxpress
    nazwaAppartementhaus Kristall at SchattbergXpress
    operatorAll in One Apartments
    miejscowoscSaalbach (centrum)
    adresSchulstraße 341, 5753 Saalbach
    gps47.3903, 12.6355
    odleglosc_od_wyciagu50 m — gondola Schattberg X-press
    ocena8.5/10 (Booking)
    uklad_dla_grupy4 osobne apartamenty (po 1 na rodzinę)
    cena_za_apartament_tydzien~1 500–2 000 € (szacunek, styczeń poza feriami)
    koszt_rodzina_2p2~1 500–2 000 €
    koszt_rodzina_2p1~1 200–1 600 €
    status_cenySZACUNEK — do potwierdzenia zapytaniem
    priorytetPLAN A
    wwwhttps://www.booking.com/hotel/at/appartementhaus-kristall.html
    ostatnia_aktualizacja2026-07-31

    W skrócie

    diff --git a/pages/oferty/sportchalet-viehhofen.html b/pages/oferty/sportchalet-viehhofen.html index e05d1c7..8ca46bb 100644 --- a/pages/oferty/sportchalet-viehhofen.html +++ b/pages/oferty/sportchalet-viehhofen.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + oferta

    Sportchalet Viehhofen — PLAN B (budżetowy faworyt)

    Kameralny obiekt holenderskich gospodarzy w Viehhofen z oceną 5.0 — najlepszy stosunek ceny do jakości, PLAN B.

    +
    Metadane (frontmatter)
    typeoferta
    titleSportchalet Viehhofen
    descriptionKameralny obiekt holenderskich gospodarzy w Viehhofen z oceną 5.0 — najlepszy stosunek ceny do jakości, PLAN B.
    timestamp2026-07-31T00:00:00Z
    tagsnocleg, viehhofen, plan-b
    idsportchalet-viehhofen
    nazwaSportchalet Viehhofen
    operatorprywatni gospodarze (NL, komunikacja EN)
    miejscowoscViehhofen (3 km od Saalbach)
    gps47.3678, 12.7351
    odleglosc_od_wyciagugondola zellamseeXpress za rogiem; skibus do Skicircusu pod domem
    ocena5.0/5 (Google)
    uklad_dla_grupy3–4 apartamenty w jednym budynku
    cena_za_apartament_tydzien~1 200–1 600 € (szacunek, styczeń poza feriami)
    koszt_rodzina_2p2~1 200–1 600 €
    koszt_rodzina_2p1~1 000–1 300 €
    status_cenySZACUNEK — wycena tylko mailowo
    priorytetPLAN B (najlepszy stosunek ceny do jakości)
    wwwhttps://sportchaletviehhofen.com
    ostatnia_aktualizacja2026-07-31

    W skrócie

    @@ -80,5 +151,20 @@ footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line);
    • Mail z zapytaniem o dostępność wszystkich apartamentów 16–23.01.2027 (opcja B: 9–16.01) + konfigurację łóżek dla 4+4+3+3.
    • Zapytać o cenę za zajęcie całego obiektu (package deal).
    • Potwierdzić rozkład skibusa w styczniu (częstotliwość rano 8:30–9:30).
    Wygenerowane z bundle'a OKF przez gen_pages.py — nie edytuj tych plików, edytuj .md i przegeneruj.
    + diff --git a/pages/porownanie.html b/pages/porownanie.html new file mode 100644 index 0000000..3f6745e --- /dev/null +++ b/pages/porownanie.html @@ -0,0 +1,184 @@ + + + + + +Porównanie ofert — Ski 2027 — Skicircus Saalbach + + + + +

    Porównanie ofert

    +

    Wszystkie 12 rozważanych noclegów w jednej tabeli — dane prosto z frontmatterów kart. Kliknij nagłówek, żeby posortować; domyślnie od najtańszej rodziny 2+2.

    +

    Regiony w skrócie

    +
    • Saalbach (baza)
      Budżet grupy: ~14 000–18 000 €

      Saalbach zostaje wariantem bazowym ze względu na najkrótszy dojazd i największy połączony teren; alternatywy biją go budżetem noclegowym.

    • Ischgl/Paznaun
      Budżet grupy: ~13 500–16 000 €

      Ischgl ma pułapkę wiekową: zniżka tylko do <17 lat — policzyć roczniki nastolatków PRZED decyzją; brak zniżki = +388 € na grupę.

    • Kronplatz
      Budżet grupy: ~13 200–16 300 €

      Dwa obiekty mają konfigurację dokładnie 4 apartamentów (Häuslerhof, Haus Central) — grupa zajmuje cały dom, co daje klimat wspólnego chaletu bez chaletowej ceny.

    • SFL
      Budżet grupy: ~12 800–15 700 €

      SFL wygrywa cenowo wśród alternatyw — jedyny region, gdzie 16–23.01 to oficjalnie niski sezon (skipassy i noclegi w dołku).

    +

    Wszystkie oferty

    +
    ZdjęcieOfertaPriorytetRegionMiejscowość€ / apart. / tydzieńRodzina 2+2Rodzina 2+1Status cenyOd wyciąguWWW
    Apartment BrandauApartment BrandauPLAN AIschgl/PaznaunKappl~900–1 300 €~900–1 300 €~750–1 100 €SZACUNEKskibus (przystanek blisko domu) → Kappl / Ischgl ~10 minkappl-apartment-brandau.at
    Apartments KapplApartments KapplPLAN BIschgl/PaznaunKappl~900–1 400 €~900–1 400 €~750–1 150 €SZACUNEKprzystanek skibusa 100 m — bez przesiadki do Ischgl, Galtür, Seeapartments.kappl.at
    HäuslerhofHäuslerhofPLAN AKronplatzValdaora/Olang~800–1 300 €~900–1 300 €~800–1 100 €SZACUNEKskibus do gondoli Olang (Kronplatz)haeuslerhof.it
    RCResidence CoronesPLAN BKronplatzValdaora~900–1 400 €~900–1 400 €~800–1 150 €SZACUNEKprzystanek skibusa PRZED domem → 5 min do gondoli Kronplatzcorones.it
    Haus Central (Apart Central)Haus Central (Apart Central)PLAN ASFLLadis~1 000–1 400 €~1 000–1 400 €~850–1 200 €SZACUNEKprzy kolejce Sonnenbahn Ladis-Fiss / przystanku skibusaserfaus-fiss-ladis.at
    AlpenParks Apartments RehrenbergAlpenParks Apartments RehrenbergPLAN CSaalbach (baza)Viehhofen~1 100–1 500 €~1 100–1 500 €~950–1 250 €SZACUNEK5–10 min pieszo do gondoli zellamseeXpressalpenparks.at
    Sportchalet ViehhofenSportchalet ViehhofenPLAN BSaalbach (baza)Viehhofen~1 200–1 600 €~1 200–1 600 €~1 000–1 300 €SZACUNEKgondola zellamseeXpress za rogiem; skibus do Skicircusu pod domemsportchaletviehhofen.com
    AIApart IdealPLAN BSFLSerfaus~1 300–1 800 €~1 300–1 800 €~1 100–1 500 €SZACUNEK50 m do stacji metra 'Tube' → <10 min do kolejkiserfaus-fiss-ladis.at
    AlpenParks Hotel & Apartment SonnleitenAlpenParks Hotel & Apartment SonnleitenPLAN A-bisSaalbach (baza)Saalbach~1 400–1 900 €~1 400–1 900 €~1 200–1 500 €SZACUNEK~2 min pieszo do stokualpenparks.at
    DDDuży dom wakacyjny 12–14 os. (do wyszukania)WARIANT RÓWNOLEGŁYSaalbach (baza)Viehhofen / Maishofen / strona Fieberbrunn4 500–6 500 € (cały dom)~1 400–1 900 €~1 100–1 500 €SZACUNEK
    AKAppartementhaus Kristall at SchattbergXpressPLAN ASaalbach (baza)Saalbach~1 500–2 000 €~1 500–2 000 €~1 200–1 600 €SZACUNEK50 m — gondola Schattberg X-pressbooking.com
    Glemm Lodge Apartments ⭐⭐⭐⭐Glemm Lodge Apartments ⭐⭐⭐⭐ODRZUCONA BUDŻETOWOSaalbach (baza)między Saalbach a Hinterglemm7 000 € + 500 € sprzątanie + taksy~4 370 €~3 277 €ZWERYFIKOWANEzjazd trasą 86 pod dom; skibus pod drzwiami (5 min do gondoli)glemmlodge-apartments.com
    +

    cena zweryfikowana   szacunek do potwierdzenia

    +
    Wygenerowane z bundle'a OKF przez gen_pages.py — nie edytuj +tych plików, edytuj .md i przegeneruj.
    + + + diff --git a/pages/region.html b/pages/region.html index eb6d700..19f4607 100644 --- a/pages/region.html +++ b/pages/region.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + region

    Skicircus Saalbach — informacje o regionie

    Wszystko poza noclegiem: teren narciarski, skipassy ALPIN CARD, dojazd z Warszawy, analiza terminów, koszty życia i budżet całkowity grupy.

    +
    Metadane (frontmatter)
    typeregion
    titleSkicircus Saalbach-Hinterglemm-Leogang-Fieberbrunn
    descriptionWszystko poza noclegiem: teren narciarski, skipassy ALPIN CARD, dojazd z Warszawy, analiza terminów, koszty życia i budżet całkowity grupy.
    timestamp2026-07-31T00:00:00Z
    tagsregion, saalbach, skipass, logistyka
    idskicircus-saalbach
    nazwaSkicircus Saalbach-Hinterglemm-Leogang-Fieberbrunn
    krajAustria
    regionSalzburg (Pinzgau)
    sezon2026/2027
    grupa4 rodziny: 2×(2+2 nastolatki) + 2×(2+1 dziecko) = 14 osób
    wyjazd_zWarszawa
    rekomendowany_termin16–23.01.2027 (alternatywa: 9–16.01.2027)
    ostatnia_aktualizacja2026-07-31

    Baza wiedzy wyjazdu narciarskiego zimą 2026/27. Ten dokument zbiera wszystko, co nie dotyczy konkretnego noclegu: teren narciarski, skipassy, dojazd, terminy, koszty życia i logistykę grupy. Oferty noclegowe → katalog oferty/.

    diff --git a/pages/sfl/oferty/apart-ideal-serfaus.html b/pages/sfl/oferty/apart-ideal-serfaus.html index 78fceed..0b567f8 100644 --- a/pages/sfl/oferty/apart-ideal-serfaus.html +++ b/pages/sfl/oferty/apart-ideal-serfaus.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + oferta

    Apart Ideal (Serfaus) — PLAN B

    Sześć apartamentów z oceną 4.8/5, 50 m od podziemnego metra w Serfaus — pełne doświadczenie regionu za dopłatą, PLAN B wariantu SFL.

    +
    Metadane (frontmatter)
    typeoferta
    titleApart Ideal (Serfaus)
    descriptionSześć apartamentów z oceną 4.8/5, 50 m od podziemnego metra w Serfaus — pełne doświadczenie regionu za dopłatą, PLAN B wariantu SFL.
    timestamp2026-08-01T00:00:00Z
    tagsnocleg, sfl, serfaus, plan-b
    idapart-ideal-serfaus
    nazwaApart Ideal
    operatorDaniel & Nina (rodzina Purtscher)
    miejscowoscSerfaus (obrzeża)
    odleglosc_od_wyciagu50 m do stacji metra 'Tube' → <10 min do kolejki
    ocena4.8/5 (13 opinii, portal regionu)
    uklad_dla_grupy6 apartamentów w domu → 4 dla nas
    cena_za_apartament_tydzien~1 300–1 800 € (szacunek; Serfaus = najdroższa wieś)
    koszt_rodzina_2p2~1 300–1 800 €
    koszt_rodzina_2p1~1 100–1 500 €
    status_cenySZACUNEK — wycena przez portal regionu
    priorytetPLAN B dla wariantu SFL
    wwwhttps://serfaus-fiss-ladis.at/en/serfaus/apartment/apart-ideal_dr91356
    ostatnia_aktualizacja2026-08-01

    W skrócie

    diff --git a/pages/sfl/oferty/haus-central-ladis.html b/pages/sfl/oferty/haus-central-ladis.html index f565290..8ca15e4 100644 --- a/pages/sfl/oferty/haus-central-ladis.html +++ b/pages/sfl/oferty/haus-central-ladis.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + oferta

    Haus Central / Apart Central (Ladis) — PLAN A

    Dokładnie cztery apartamenty z balkonami przy kolejce Sonnenbahn w najtańszej wsi płaskowyżu — cały dom dla grupy, PLAN A wariantu SFL.

    +
    Metadane (frontmatter)
    typeoferta
    titleHaus Central (Apart Central), Ladis
    descriptionDokładnie cztery apartamenty z balkonami przy kolejce Sonnenbahn w najtańszej wsi płaskowyżu — cały dom dla grupy, PLAN A wariantu SFL.
    timestamp2026-08-01T00:00:00Z
    tagsnocleg, sfl, ladis, plan-a
    idhaus-central-ladis
    nazwaHaus Central (Apart Central)
    operatorprywatny
    miejscowoscLadis
    odleglosc_od_wyciaguprzy kolejce Sonnenbahn Ladis-Fiss / przystanku skibusa
    uklad_dla_grupyDOKŁADNIE 4 apartamenty, każdy z balkonem — cały dom dla nas
    cena_za_apartament_tydzien~1 000–1 400 € (szacunek, niski sezon 16–23.01)
    koszt_rodzina_2p2~1 000–1 400 €
    koszt_rodzina_2p1~850–1 200 €
    status_cenySZACUNEK — wycena przez serfaus-fiss-ladis.at lub mailowo
    priorytetPLAN A dla wariantu SFL
    wwwhttps://www.serfaus-fiss-ladis.at/en/Accommodation-list/Haus-Central-holiday-apartment-Ladis_ad-40835
    ostatnia_aktualizacja2026-08-01

    W skrócie

    @@ -76,5 +147,20 @@ footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line);
    • Zapytanie przez system serfaus-fiss-ladis.at (przycisk enquiry) lub mail: 4 apartamenty, 16–23.01.2027, 4+4+3+3.
    • Alternatywa w Ladis: Laudegghof (4★ apartamenty, jacuzzi/spa, 8.0 na Expedia, sprzątanie 100–120 €/apartament).
    Wygenerowane z bundle'a OKF przez gen_pages.py — nie edytuj tych plików, edytuj .md i przegeneruj.
    + diff --git a/pages/sfl/region.html b/pages/sfl/region.html index 270d890..a6ddb21 100644 --- a/pages/sfl/region.html +++ b/pages/sfl/region.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + region

    Serfaus-Fiss-Ladis — informacje o regionie

    Wariant alternatywny i najtańszy budżetowo: 214 km na słonecznym płaskowyżu, infrastruktura skrojona pod rodziny, a termin 16–23.01 to u nich oficjalnie niski sezon.

    +
    Metadane (frontmatter)
    typeregion
    titleSerfaus-Fiss-Ladis (SFL)
    descriptionWariant alternatywny i najtańszy budżetowo: 214 km na słonecznym płaskowyżu, infrastruktura skrojona pod rodziny, a termin 16–23.01 to u nich oficjalnie niski sezon.
    timestamp2026-08-01T00:00:00Z
    tagsregion, sfl, wariant, skipass, logistyka
    idserfaus-fiss-ladis
    nazwaSerfaus-Fiss-Ladis (SFL)
    krajAustria
    regionTyrol (górna dolina Innu)
    sezon2026/2027
    grupa4 rodziny (2×2+2, 2×2+1) = 14 osób
    wyjazd_zWarszawa
    rekomendowany_termin16–23.01.2027 — u nich to NISKI SEZON (taniej!)
    strategia_budzetowamieszkać w Fiss lub Ladis (do ~30% taniej niż Serfaus)
    ostatnia_aktualizacja2026-08-01

    Najbardziej "rodzinny" kurort Alp: słoneczny płaskowyż na 1 200–1 400 m, 214 km tras i 68 wyciągów, śnieg pewny do wiosny (jazda do 2 820 m). Wielokrotny zwycięzca rankingów family-friendly. Dla naszej mieszanki wiekowej (nastolatki + młodsze dzieci) teren skrojony idealnie: dwa snowparki (Freestyle Circus Serfaus, Fun Area Fiss-Ladis), funslope'y, a w Serfaus kultowe metro podziemne (wioska bez aut).

    diff --git a/pages/start.html b/pages/start.html new file mode 100644 index 0000000..f49c354 --- /dev/null +++ b/pages/start.html @@ -0,0 +1,143 @@ + + + + + +Start — Ski 2027 — Skicircus Saalbach + + + + +

    Ski 2027 — Skicircus Saalbach

    +

    16–23.01.2027, 4 rodziny / 14 osób, wyjazd z Warszawy. Od czego zacząć:

    + +

    Regiony

    +

    Karta regionu = skipassy, dojazd, terminy, koszty życia i budżet całkowity.

    + +
    Wygenerowane z bundle'a OKF przez gen_pages.py — nie edytuj +tych plików, edytuj .md i przegeneruj.
    + + diff --git a/pages/szablon-maila.html b/pages/szablon-maila.html index 8febdde..a10b3b6 100644 --- a/pages/szablon-maila.html +++ b/pages/szablon-maila.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + narzedzie

    Szablon maila — zapytanie grupowe (EN)

    Gotowy angielski mail z zapytaniem o 4 apartamenty dla 14 osób w terminie 16–23.01.2027 wraz z notatkami negocjacyjnymi.

    +
    Metadane (frontmatter)
    typenarzedzie
    titleSzablon zapytania grupowego (EN)
    descriptionGotowy angielski mail z zapytaniem o 4 apartamenty dla 14 osób w terminie 16–23.01.2027 wraz z notatkami negocjacyjnymi.
    timestamp2026-07-31T00:00:00Z
    tagsszablon, komunikacja, rezerwacja
    idszablon-maila
    nazwaSzablon zapytania grupowego (EN)
    ostatnia_aktualizacja2026-07-31

    Wysłać ten sam mail do 2–3 obiektów równolegle, podmieniając [pola]. Wersja uniwersalna pod 4 apartamenty; przy Sportchalet/Glemm Lodge dopisać pytanie o zajęcie całego obiektu.

    diff --git a/pages/wyjazd.html b/pages/wyjazd.html index bd424c0..551771d 100644 --- a/pages/wyjazd.html +++ b/pages/wyjazd.html @@ -57,13 +57,84 @@ th { background: var(--chip); font-weight: 600; } .cards p { margin: .3rem 0 0; font-size: .9rem; color: var(--muted); } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); font-size: .78rem; color: var(--muted); } + +/* landing: kafle i lista regionów */ +body.wide { max-width: 72rem; } +.tiles, .regions { list-style: none; padding: 0; } +.tiles { margin: 1.25rem 0 2rem; display: grid; gap: .75rem; } +.regions { margin: .6rem 0 0; display: grid; gap: .5rem; } +@media (min-width: 34rem) { + .tiles { grid-template-columns: repeat(3, 1fr); } + .regions { grid-template-columns: repeat(2, 1fr); } +} +.tiles a, .regions a { display: block; border: 1px solid var(--line); border-radius: 10px; + padding: .9rem 1rem; text-decoration: none; color: var(--fg); } +.tiles a { font-size: 1.05rem; font-weight: 600; } +.tiles a:hover, .tiles a:focus, .regions a:hover, .regions a:focus { + border-color: var(--accent); } +.tiles small, .regions small { display: block; margin-top: .3rem; font-weight: 400; + font-size: .85rem; color: var(--muted); } +.regions a { font-weight: 600; } + +/* podsumowanie per region na karcie porównania */ +.summary { list-style: none; padding: 0; margin: .8rem 0 2rem; display: grid; gap: .6rem; } +@media (min-width: 48rem) { .summary { grid-template-columns: repeat(2, 1fr); } } +.summary li { border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 8px; padding: .65rem .85rem; } +.summary .budget { font-size: .95rem; } +.summary p { margin: .35rem 0 0; font-size: .88rem; color: var(--muted); } + +/* tabela porównawcza: sortowanie klikiem + kolor statusu ceny */ +table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +table.sortable th::after { content: " ↕"; font-size: .8em; opacity: .35; } +table.sortable th[data-dir="asc"]::after { content: " ↑"; opacity: 1; } +table.sortable th[data-dir="desc"]::after { content: " ↓"; opacity: 1; } +table.sortable td { font-size: .88rem; padding: .35rem .5rem; } +table.sortable td[data-num] { white-space: nowrap; } +table.sortable .name { font-weight: 600; } +tr.verified td { background: rgba(13, 148, 136, .14); } +tr.estimate td { background: rgba(100, 116, 139, .10); color: var(--muted); } +tr.estimate td.name a, tr.verified td.name a { color: var(--accent); } +.legend { font-size: .82rem; color: var(--muted); margin: .5rem 0 0; } +.legend .box { display: inline-block; width: .8em; height: .8em; border-radius: 3px; + border: 1px solid var(--line); vertical-align: -.05em; margin-right: .25em; } +.legend .box.v { background: rgba(13, 148, 136, .35); } +.legend .box.e { background: rgba(100, 116, 139, .25); } + +/* miniatury w tabeli */ +td.thumb { padding: .25rem; width: 128px; } +td.thumb img { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; + display: block; } +td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius: 6px; + align-items: center; justify-content: center; background: var(--chip); + color: var(--muted); font-weight: 700; letter-spacing: .05em; + text-decoration: none; } + +/* galeria na karcie oferty */ +.gallery { margin: 0 0 1.25rem; } +.gallery-main img { width: 100%; max-height: 24rem; object-fit: cover; + border-radius: 10px; display: block; background: var(--chip); } +.gallery-thumbs { display: flex; gap: .4rem; margin-top: .4rem; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: .2rem; } +.gallery-thumbs button { flex: 0 0 auto; padding: 0; border: 2px solid transparent; + border-radius: 6px; background: none; cursor: pointer; } +.gallery-thumbs button[aria-current] { border-color: var(--accent); } +.gallery-thumbs img { width: 5.5rem; height: 3.6rem; object-fit: cover; + border-radius: 4px; display: block; } + +/* kafel regionu ze zdjęciem w tle */ +.regions a.hero { background-size: cover; background-position: center; + border-color: transparent; color: #f8fafc; min-height: 6.5rem; + display: flex; flex-direction: column; justify-content: flex-end; } +.regions a.hero small { color: #e2e8f0; } - + Trip

    Ski 2027 — Skicircus Saalbach

    Wyjazd narciarski 4 rodzin (14 osób) z Warszawy do Skicircus Saalbach w styczniu 2027, z celem budżetowym ~2 000 € za nocleg na rodzinę.

    +
    Metadane (frontmatter)
    typeTrip
    titleSki 2027 — Skicircus Saalbach
    descriptionWyjazd narciarski 4 rodzin (14 osób) z Warszawy do Skicircus Saalbach w styczniu 2027, z celem budżetowym ~2 000 € za nocleg na rodzinę.
    timestamp2026-07-31T00:00:00Z
    tagswyjazd, saalbach, 2027, planowanie
    nazwaSki 2027 — Skicircus Saalbach: baza wiedzy wyjazdu
    grupa4 rodziny (2×2+2, 2×2+1) = 14 osób, wyjazd z Warszawy
    termin16–23.01.2027 (alt. 9–16.01)
    ostatnia_aktualizacja2026-07-31

    Wyjazd narciarski 4 rodzin: 2×(2+2 nastolatki) + 2×(2+1 dziecko) = 14 osób, wyjazd samochodami z Warszawy.