Karty HTML konceptów: gen_pages.py + katalog pages/

Graf pokazywał strukturę, ale treść (ceny, dostępność, plusy/minusy)
siedziała tylko w panelu szczegółów viewera. gen_pages.py renderuje każdy
koncept do pages/<id>.html: metadane frontmattera jako tabelka (wszystkie
pola, też polskie rozszerzenia), body poniżej, plus pages/index.html
z listą kart.

Renderer: python-markdown, jeśli jest w systemie; tutaj go nie ma, więc
działa fallback na samej bibliotece standardowej (nagłówki, akapity,
tabele GFM, listy z - [ ], cytaty, kod, hr, inline bold/kursywa/kod/linki).

Linki miedzy konceptami przepisane na względne <id>.html, linki
zewnętrzne bez zmian. Pliki zarezerwowane (index.md, log.md) nie są
konceptami (§3.1) i nie mają kart — linki do nich prowadzą do listy kart.

Pliki .md nietknięte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
oskar 2026-07-31 18:46:08 +02:00
parent 8cca477153
commit c4f6cf3f1d
11 changed files with 1387 additions and 0 deletions

549
gen_pages.py Normal file
View file

@ -0,0 +1,549 @@
#!/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).
Renderer markdown: używa `python-markdown`, jeśli jest zainstalowany
(rozszerzenia tables + fenced_code + sane_lists); w przeciwnym razie własnego
renderera na samej bibliotece standardowej obsługuje nagłówki, akapity,
tabele GFM, listy (w tym `- [ ]`), cytaty, bloki kodu, linie poziome oraz
inline: **bold**, *kursywa*, `kod`, [linki](...). Który tor zadziałał, skrypt
wypisuje przy uruchomieniu.
Pliki .md NIE modyfikowane to czysty generator, tak samo jak gen_viz.py.
Pliki zarezerwowane (index.md, log.md) nie konceptami (§3.1) i nie
dostają kart; linki do nich prowadzą do pages/index.html.
Uruchomienie: python3 gen_pages.py
"""
from __future__ import annotations
import html
import posixpath
import re
import shutil
import sys
from dataclasses import dataclass
from pathlib import Path
import yaml
BUNDLE_ROOT = Path(__file__).resolve().parent
PAGES_DIRNAME = "pages"
PAGES_DIR = BUNDLE_ROOT / PAGES_DIRNAME
BUNDLE_NAME = "Ski 2027 — Skicircus Saalbach"
RESERVED = {"index.md", "log.md"}
try: # renderer preferowany, jeśli dostępny w systemie
import markdown as _markdown_lib
except ModuleNotFoundError:
_markdown_lib = None
RENDERER = "python-markdown" if _markdown_lib else "fallback (biblioteka standardowa)"
# --- model ------------------------------------------------------------
@dataclass
class Concept:
id: str # np. "oferty/glemm-lodge"
path: Path
frontmatter: dict
body: str
@property
def page(self) -> str:
"""Ścieżka karty względem katalogu pages/."""
return f"{self.id}.html"
@property
def title(self) -> str:
return str(self.frontmatter.get("title") or self.id)
def load_concepts() -> list[Concept]:
concepts: list[Concept] = []
for path in sorted(BUNDLE_ROOT.rglob("*.md")):
if ".git" in path.parts or path.name in RESERVED:
continue
text = path.read_text(encoding="utf-8")
frontmatter: dict = {}
body = text
if text.startswith("---\n"):
end = text.find("\n---", 3)
if end != -1:
frontmatter = yaml.safe_load(text[4:end]) or {}
body = text[end + 4 :]
concepts.append(
Concept(
id=path.relative_to(BUNDLE_ROOT).with_suffix("").as_posix(),
path=path,
frontmatter=frontmatter,
body=body.lstrip("\n"),
)
)
return concepts
# --- linki ------------------------------------------------------------
_LINK_TARGET_RE = re.compile(r"\]\(([^)\s]+\.md)((?:#[^)\s]*)?)\)")
def rewrite_links(body: str, source: Concept, pages: dict[str, str]) -> str:
"""Linki do plików .md → względne ścieżki do kart. External bez zmian."""
here = posixpath.dirname(source.page)
def repl(match: re.Match) -> str:
target, fragment = match.group(1), match.group(2)
if "://" in target:
return match.group(0)
if target.startswith("/"):
rel = target[1:]
else:
rel = posixpath.normpath(posixpath.join(posixpath.dirname(source.id), target))
concept_id = rel[:-3] if rel.endswith(".md") else rel
if concept_id in pages:
dest = pages[concept_id]
elif posixpath.basename(rel) in RESERVED:
dest = "index.html" # index.md / log.md nie mają kart
else:
return match.group(0)
return "](" + posixpath.relpath(dest, here or ".") + fragment + ")"
return _LINK_TARGET_RE.sub(repl, body)
# --- renderer markdown: fallback na stdlib -----------------------------
_CODE_RE = re.compile(r"`([^`]+)`")
_LINK_RE = re.compile(r"\[([^\]]*)\]\(([^)\s]+)\)")
_BOLD_RE = re.compile(r"\*\*(.+?)\*\*", re.S)
_ITALIC_STAR_RE = re.compile(r"(?<!\*)\*(?!\s)([^*]+?)(?<!\s)\*(?!\*)")
_ITALIC_UNDER_RE = re.compile(r"(?<![\w\\])_(?!\s)([^_]+?)(?<!\s)_(?![\w])")
_HEADING_RE = re.compile(r"^(#{1,6})\s+(.*)$")
_HR_RE = re.compile(r"^(-{3,}|\*{3,}|_{3,})$")
_LIST_RE = re.compile(r"^\s*([-*+]|\d+[.)])\s+(.*)$")
_TASK_RE = re.compile(r"^\[([ xX])\]\s+(.*)$")
_TABLE_SEP_RE = re.compile(r"^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$")
def render_inline(text: str) -> str:
codes: list[str] = []
def stash(match: re.Match) -> str:
codes.append(match.group(1))
return f"\x00c{len(codes) - 1}\x00"
text = _CODE_RE.sub(stash, text)
text = html.escape(text, quote=False)
def link(match: re.Match) -> str:
label, href = match.group(1), match.group(2)
attrs = ""
if "://" in href or href.startswith("mailto:"):
attrs = ' target="_blank" rel="noopener" class="ext"'
return f'<a href="{html.escape(href, quote=True)}"{attrs}>{label}</a>'
text = _LINK_RE.sub(link, text)
text = _BOLD_RE.sub(r"<strong>\1</strong>", text)
text = _ITALIC_STAR_RE.sub(r"<em>\1</em>", text)
text = _ITALIC_UNDER_RE.sub(r"<em>\1</em>", text)
for i, code in enumerate(codes):
text = text.replace(f"\x00c{i}\x00", f"<code>{html.escape(code, quote=False)}</code>")
return text
def _alignments(sep: str) -> list[str]:
out = []
for cell in _cells(sep):
left, right = cell.startswith(":"), cell.endswith(":")
out.append("center" if left and right else "right" if right else "left")
return out
def _cells(row: str) -> list[str]:
return [c.strip() for c in row.strip().strip("|").split("|")]
def _starts_block(line: str) -> bool:
s = line.strip()
return (
not s
or bool(_HEADING_RE.match(s))
or bool(_HR_RE.match(s))
or s.startswith(("```", ">"))
or bool(_LIST_RE.match(line))
)
def render_blocks(text: str) -> str:
lines = text.split("\n")
out: list[str] = []
i = 0
while i < len(lines):
line = lines[i]
stripped = line.strip()
if not stripped:
i += 1
continue
if stripped.startswith("```"):
i += 1
code: list[str] = []
while i < len(lines) and not lines[i].strip().startswith("```"):
code.append(lines[i])
i += 1
i += 1
out.append(f"<pre><code>{html.escape(chr(10).join(code), quote=False)}</code></pre>")
continue
heading = _HEADING_RE.match(stripped)
if heading:
level = len(heading.group(1))
out.append(f"<h{level}>{render_inline(heading.group(2))}</h{level}>")
i += 1
continue
if _HR_RE.match(stripped):
out.append("<hr>")
i += 1
continue
if "|" in stripped and i + 1 < len(lines) and _TABLE_SEP_RE.match(lines[i + 1]):
header = _cells(stripped)
align = _alignments(lines[i + 1])
i += 2
rows = []
while i < len(lines) and "|" in lines[i] and lines[i].strip():
rows.append(_cells(lines[i]))
i += 1
head = "".join(
f'<th style="text-align:{align[n] if n < len(align) else "left"}">'
f"{render_inline(c)}</th>"
for n, c in enumerate(header)
)
body = "".join(
"<tr>"
+ "".join(
f'<td style="text-align:{align[n] if n < len(align) else "left"}">'
f"{render_inline(c)}</td>"
for n, c in enumerate(row)
)
+ "</tr>"
for row in rows
)
out.append(
f'<div class="table-wrap"><table><thead><tr>{head}</tr></thead>'
f"<tbody>{body}</tbody></table></div>"
)
continue
if stripped.startswith(">"):
quoted = []
while i < len(lines) and lines[i].strip().startswith(">"):
quoted.append(lines[i].strip()[1:].lstrip())
i += 1
out.append(f"<blockquote>{render_blocks(chr(10).join(quoted))}</blockquote>")
continue
item = _LIST_RE.match(line)
if item:
ordered = item.group(1)[0].isdigit()
items: list[str] = []
while i < len(lines):
current = lines[i]
match = _LIST_RE.match(current)
if match:
items.append(match.group(2))
i += 1
elif current.startswith((" ", "\t")) and current.strip() and items:
items[-1] += " " + current.strip()
i += 1
else:
break
rendered = []
for raw in items:
task = _TASK_RE.match(raw)
if task:
checked = " checked" if task.group(1).lower() == "x" else ""
rendered.append(
f'<li class="task"><input type="checkbox" disabled{checked}> '
f"{render_inline(task.group(2))}</li>"
)
else:
rendered.append(f"<li>{render_inline(raw)}</li>")
tag = "ol" if ordered else "ul"
out.append(f"<{tag}>{''.join(rendered)}</{tag}>")
continue
paragraph = [stripped]
i += 1
while i < len(lines) and not _starts_block(lines[i]):
if "|" in lines[i] and i + 1 < len(lines) and _TABLE_SEP_RE.match(lines[i + 1]):
break
paragraph.append(lines[i].strip())
i += 1
out.append(f"<p>{render_inline(' '.join(paragraph))}</p>")
return "\n".join(out)
def md_to_html(text: str) -> str:
if _markdown_lib:
return _markdown_lib.markdown(
text, extensions=["tables", "fenced_code", "sane_lists"]
)
return render_blocks(text)
# --- szablon ----------------------------------------------------------
CSS = """
:root { color-scheme: light dark; --fg:#0f172a; --muted:#64748b; --bg:#ffffff;
--line:#e2e8f0; --accent:#0d9488; --chip:#f1f5f9; --quote:#f8fafc; }
@media (prefers-color-scheme: dark) {
:root { --fg:#e2e8f0; --muted:#94a3b8; --bg:#0f172a; --line:#334155;
--accent:#2dd4bf; --chip:#1e293b; --quote:#1e293b; }
}
* { box-sizing: border-box; }
body { margin: 0 auto; padding: 1.25rem 1rem 4rem; max-width: 46rem;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 17px; line-height: 1.55; color: var(--fg); background: var(--bg);
-webkit-text-size-adjust: 100%; }
nav { font-size: .85rem; margin-bottom: 1.25rem; }
nav a { color: var(--muted); }
h1 { font-size: 1.55rem; line-height: 1.25; margin: 0 0 .35rem; }
h2 { font-size: 1.2rem; margin: 2rem 0 .5rem; padding-top: .5rem;
border-top: 1px solid var(--line); }
h3 { font-size: 1.02rem; margin: 1.4rem 0 .4rem; }
p, ul, ol { margin: .6rem 0; }
ul, ol { padding-left: 1.3rem; }
li { margin: .25rem 0; }
li.task { list-style: none; margin-left: -1.3rem; }
li.task input { margin-right: .45rem; }
a { color: var(--accent); }
a.ext::after { content: ""; font-size: .8em; color: var(--muted); }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em;
background: var(--chip); padding: .1em .35em; border-radius: 4px; }
pre { background: var(--chip); padding: .75rem; border-radius: 6px; overflow-x: auto; }
pre code { background: none; padding: 0; }
blockquote { margin: .9rem 0; padding: .6rem .9rem; background: var(--quote);
border-left: 3px solid var(--accent); border-radius: 0 6px 6px 0; }
blockquote p { margin: .2rem 0; }
hr { border: 0; border-top: 1px solid var(--line); margin: 1.5rem 0; }
.table-wrap { overflow-x: auto; margin: .8rem 0; -webkit-overflow-scrolling: touch; }
table { border-collapse: collapse; width: 100%; font-size: .92rem; }
th, td { border: 1px solid var(--line); padding: .4rem .55rem; vertical-align: top; }
th { background: var(--chip); font-weight: 600; }
.chip { display: inline-block; background: var(--chip); color: var(--muted);
font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
padding: .15rem .5rem; border-radius: 999px; }
.lead { color: var(--muted); margin: .35rem 0 1.25rem; }
.meta { margin: 0 0 1.5rem; }
.meta summary { cursor: pointer; font-size: .85rem; color: var(--muted);
padding: .3rem 0; }
.meta table { font-size: .85rem; }
.meta th { width: 38%; text-align: left; font-weight: 500; word-break: break-word; }
.cards { list-style: none; padding: 0; margin: 0; }
.cards li { border: 1px solid var(--line); border-radius: 8px; padding: .8rem .9rem;
margin: .6rem 0; }
.cards a { font-weight: 600; text-decoration: none; }
.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); }
"""
def _page_shell(title: str, depth: int, content: str) -> str:
up = "../" * depth
return f"""<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{html.escape(title)} {html.escape(BUNDLE_NAME)}</title>
<style>{CSS}</style>
</head>
<body>
<nav><a href="{up}index.html"> Wszystkie karty</a> · <a href="{up}../viz.html">Graf</a></nav>
{content}
<footer>Wygenerowane z bundle'a OKF przez <code>gen_pages.py</code> — nie edytuj
tych plików, edytuj <code>.md</code> i przegeneruj.</footer>
</body>
</html>
"""
def _meta_table(frontmatter: dict) -> str:
rows = []
for key, value in frontmatter.items():
if isinstance(value, list):
shown = ", ".join(_link_value(str(v)) for v in value)
else:
shown = _link_value(str(value))
rows.append(
f"<tr><th>{html.escape(str(key))}</th><td>{shown}</td></tr>"
)
return (
'<details class="meta" open><summary>Metadane (frontmatter)</summary>'
f'<div class="table-wrap"><table><tbody>{"".join(rows)}</tbody></table></div>'
"</details>"
)
_URLISH_RE = re.compile(
r"^(https?://\S+|(?:[a-z0-9][a-z0-9-]*\.)+(?:at|com|pl|de|eu|net|org|io|info))$"
)
def _link_value(value: str) -> str:
"""Wartość frontmattera; adresy robi klikalnymi, resztę tylko escape'uje."""
escaped = html.escape(value, quote=False)
if _URLISH_RE.match(value.strip()):
href = value.strip()
if "://" not in href:
href = "https://" + href
return f'<a href="{html.escape(href, quote=True)}" target="_blank" rel="noopener" class="ext">{escaped}</a>'
if value.strip().startswith("mailto:") or "@" in value and " " not in value.strip():
return f'<a href="mailto:{html.escape(value.strip(), quote=True)}">{escaped}</a>'
return escaped
def render_page(concept: Concept, pages: dict[str, str]) -> str:
body = rewrite_links(concept.body, concept, pages)
rendered = md_to_html(body)
# Pierwszy nagłówek H1 body dubluje tytuł karty — awansujemy go na <h1>
# strony zamiast dokładać drugi. Sam `title` i tak jest w tabelce metadanych.
match = re.match(r"\s*<h1>(.*?)</h1>", rendered, re.S)
if match:
headline = match.group(1)
rendered = rendered[match.end() :]
else:
headline = html.escape(concept.title)
frontmatter = concept.frontmatter
chip = html.escape(str(frontmatter.get("type") or ""))
description = str(frontmatter.get("description") or "")
lead = f'<p class="lead">{html.escape(description)}</p>' if description else ""
content = (
f'<span class="chip">{chip}</span>\n'
f"<h1>{headline}</h1>\n{lead}\n"
f"{_meta_table(frontmatter)}\n{rendered}"
)
return _page_shell(concept.title, concept.id.count("/"), content)
def render_index(concepts: list[Concept]) -> str:
groups: list[tuple[str, list[Concept]]] = [
("Wyjazd i wiedza ogólna", [c for c in concepts if "/" not in c.id]),
("Oferty noclegowe", [c for c in concepts if c.id.startswith("oferty/")]),
]
sections = []
for heading, items in groups:
if not items:
continue
cards = []
for concept in items:
description = str(concept.frontmatter.get("description") or "")
status = concept.frontmatter.get("status_ceny")
badge = (
f'<span class="chip">{html.escape(str(status).split("")[0].strip())}</span> '
if status
else ""
)
cards.append(
f'<li><a href="{html.escape(concept.page, quote=True)}">'
f"{html.escape(concept.title)}</a> "
f'{badge}<span class="chip">{html.escape(str(concept.frontmatter.get("type") or ""))}</span>'
f"<p>{html.escape(description)}</p></li>"
)
sections.append(f"<h2>{heading}</h2>\n<ul class=\"cards\">{''.join(cards)}</ul>")
content = (
f"<h1>{html.escape(BUNDLE_NAME)}</h1>\n"
'<p class="lead">Karty konceptów bundle\'a — pełna treść każdego dokumentu '
"(ceny, dostępność, plusy/minusy). Struktura powiązań: "
'<a href="../viz.html">graf</a>.</p>\n' + "\n".join(sections)
)
return _page_shell("Karty", 0, content)
# --- raport braków ----------------------------------------------------
def missing_data(concepts: list[Concept]) -> list[tuple[str, list[str]]]:
"""Czego brakuje w ofertach — bez zgadywania, tylko z tego, co jest."""
report = []
for concept in concepts:
if not str(concept.frontmatter.get("type", "")).startswith("oferta"):
continue
gaps = []
status = str(concept.frontmatter.get("status_ceny") or "")
if not status:
gaps.append("brak pola `status_ceny`")
elif "SZACUNEK" in status.upper():
gaps.append(f"cena niepotwierdzona ({status})")
# Danymi o dostępności jest osobna sekcja (jak w glemm-lodge), a nie
# samo słowo w tekście — "duża dostępność" w opisie się nie liczy.
if not re.search(r"^#+\s*.*dostępnoś", concept.body, re.IGNORECASE | re.M):
gaps.append("brak danych o dostępności terminu 1623.01.2027")
open_todos = len(re.findall(r"^\s*[-*]\s+\[ \]", concept.body, re.M))
if open_todos:
gaps.append(f"otwartych pozycji „Do zrobienia”: {open_todos}")
if gaps:
report.append((concept.id, gaps))
return report
# --- main -------------------------------------------------------------
def main() -> int:
concepts = load_concepts()
pages = {c.id: c.page for c in concepts}
if PAGES_DIR.exists():
shutil.rmtree(PAGES_DIR)
PAGES_DIR.mkdir(parents=True)
written = []
for concept in concepts:
out = PAGES_DIR / concept.page
out.parent.mkdir(parents=True, exist_ok=True)
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)
print(f"Renderer markdown: {RENDERER}")
print(f"Zapisano {len(written)} plików w {PAGES_DIR}:")
for path in written:
size = path.stat().st_size / 1024
print(f" {path.relative_to(BUNDLE_ROOT)} ({size:.1f} KiB)")
gaps = missing_data(concepts)
print()
if gaps:
print(f"Oferty z brakami w danych ({len(gaps)}) — do uzupełnienia ręcznie:")
for concept_id, items in gaps:
print(f" ! {concept_id}")
for item in items:
print(f" {item}")
else:
print("Braków w danych ofert nie wykryto.")
return 0
if __name__ == "__main__":
raise SystemExit(main())

73
pages/index.html Normal file
View file

@ -0,0 +1,73 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Karty — Ski 2027 — Skicircus Saalbach</title>
<style>
:root { color-scheme: light dark; --fg:#0f172a; --muted:#64748b; --bg:#ffffff;
--line:#e2e8f0; --accent:#0d9488; --chip:#f1f5f9; --quote:#f8fafc; }
@media (prefers-color-scheme: dark) {
:root { --fg:#e2e8f0; --muted:#94a3b8; --bg:#0f172a; --line:#334155;
--accent:#2dd4bf; --chip:#1e293b; --quote:#1e293b; }
}
* { box-sizing: border-box; }
body { margin: 0 auto; padding: 1.25rem 1rem 4rem; max-width: 46rem;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 17px; line-height: 1.55; color: var(--fg); background: var(--bg);
-webkit-text-size-adjust: 100%; }
nav { font-size: .85rem; margin-bottom: 1.25rem; }
nav a { color: var(--muted); }
h1 { font-size: 1.55rem; line-height: 1.25; margin: 0 0 .35rem; }
h2 { font-size: 1.2rem; margin: 2rem 0 .5rem; padding-top: .5rem;
border-top: 1px solid var(--line); }
h3 { font-size: 1.02rem; margin: 1.4rem 0 .4rem; }
p, ul, ol { margin: .6rem 0; }
ul, ol { padding-left: 1.3rem; }
li { margin: .25rem 0; }
li.task { list-style: none; margin-left: -1.3rem; }
li.task input { margin-right: .45rem; }
a { color: var(--accent); }
a.ext::after { content: " ↗"; font-size: .8em; color: var(--muted); }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em;
background: var(--chip); padding: .1em .35em; border-radius: 4px; }
pre { background: var(--chip); padding: .75rem; border-radius: 6px; overflow-x: auto; }
pre code { background: none; padding: 0; }
blockquote { margin: .9rem 0; padding: .6rem .9rem; background: var(--quote);
border-left: 3px solid var(--accent); border-radius: 0 6px 6px 0; }
blockquote p { margin: .2rem 0; }
hr { border: 0; border-top: 1px solid var(--line); margin: 1.5rem 0; }
.table-wrap { overflow-x: auto; margin: .8rem 0; -webkit-overflow-scrolling: touch; }
table { border-collapse: collapse; width: 100%; font-size: .92rem; }
th, td { border: 1px solid var(--line); padding: .4rem .55rem; vertical-align: top; }
th { background: var(--chip); font-weight: 600; }
.chip { display: inline-block; background: var(--chip); color: var(--muted);
font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
padding: .15rem .5rem; border-radius: 999px; }
.lead { color: var(--muted); margin: .35rem 0 1.25rem; }
.meta { margin: 0 0 1.5rem; }
.meta summary { cursor: pointer; font-size: .85rem; color: var(--muted);
padding: .3rem 0; }
.meta table { font-size: .85rem; }
.meta th { width: 38%; text-align: left; font-weight: 500; word-break: break-word; }
.cards { list-style: none; padding: 0; margin: 0; }
.cards li { border: 1px solid var(--line); border-radius: 8px; padding: .8rem .9rem;
margin: .6rem 0; }
.cards a { font-weight: 600; text-decoration: none; }
.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); }
</style>
</head>
<body>
<nav><a href="index.html">← Wszystkie karty</a> · <a href="../viz.html">Graf</a></nav>
<h1>Ski 2027 — Skicircus Saalbach</h1>
<p class="lead">Karty konceptów bundle'a — pełna treść każdego dokumentu (ceny, dostępność, plusy/minusy). Struktura powiązań: <a href="../viz.html">graf</a>.</p>
<h2>Wyjazd i wiedza ogólna</h2>
<ul class="cards"><li><a href="region.html">Skicircus Saalbach-Hinterglemm-Leogang-Fieberbrunn</a> <span class="chip">region</span><p>Wszystko poza noclegiem: teren narciarski, skipassy ALPIN CARD, dojazd z Warszawy, analiza terminów, koszty życia i budżet całkowity grupy.</p></li><li><a href="szablon-maila.html">Szablon zapytania grupowego (EN)</a> <span class="chip">narzedzie</span><p>Gotowy angielski mail z zapytaniem o 4 apartamenty dla 14 osób w terminie 1623.01.2027 wraz z notatkami negocjacyjnymi.</p></li><li><a href="wyjazd.html">Ski 2027 — Skicircus Saalbach</a> <span class="chip">Trip</span><p>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ę.</p></li></ul>
<h2>Oferty noclegowe</h2>
<ul class="cards"><li><a href="oferty/alpenparks-rehrenberg.html">AlpenParks Apartments Rehrenberg</a> <span class="chip">SZACUNEK</span> <span class="chip">oferta</span><p>Najtańszy nowy obiekt na liście — przestronne apartamenty z prywatnymi saunami w Viehhofen, wariant rezerwowy o mieszanych opiniach, PLAN C.</p></li><li><a href="oferty/alpenparks-sonnleiten.html">AlpenParks Hotel &amp; Apartment Sonnleiten</a> <span class="chip">SZACUNEK</span> <span class="chip">oferta</span><p>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.</p></li><li><a href="oferty/dom-dla-grupy.html">Duży dom wakacyjny 1214 os. (do wyszukania)</a> <span class="chip">SZACUNEK</span> <span class="chip">oferta-koncept</span><p>Koncept wynajęcia jednego domu wakacyjnego dla całej grupy zamiast czterech osobnych apartamentów — klimat chaletowy w połowie ceny Glemm Lodge.</p></li><li><a href="oferty/glemm-lodge.html">Glemm Lodge Apartments ⭐⭐⭐⭐</a> <span class="chip">ZWERYFIKOWANE (cennik + kalendarz obiektu, stan 31.07.2026)</span> <span class="chip">oferta</span><p>Jedyna oferta z w pełni zweryfikowanym cennikiem (~4 370 €/rodzinę) — odrzucona budżetowo, służy jako punkt odniesienia cenowego.</p></li><li><a href="oferty/kristall-schattbergxpress.html">Appartementhaus Kristall at SchattbergXpress</a> <span class="chip">SZACUNEK</span> <span class="chip">oferta</span><p>Apartamenty 50 m od gondoli Schattberg X-press w centrum Saalbach — najlepsza lokalizacja w rozważanym budżecie, PLAN A.</p></li><li><a href="oferty/sportchalet-viehhofen.html">Sportchalet Viehhofen</a> <span class="chip">SZACUNEK</span> <span class="chip">oferta</span><p>Kameralny obiekt holenderskich gospodarzy w Viehhofen z oceną 5.0 — najlepszy stosunek ceny do jakości, PLAN B.</p></li></ul>
<footer>Wygenerowane z bundle'a OKF przez <code>gen_pages.py</code> — nie edytuj
tych plików, edytuj <code>.md</code> i przegeneruj.</footer>
</body>
</html>

View file

@ -0,0 +1,83 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AlpenParks Apartments Rehrenberg — Ski 2027 — Skicircus Saalbach</title>
<style>
:root { color-scheme: light dark; --fg:#0f172a; --muted:#64748b; --bg:#ffffff;
--line:#e2e8f0; --accent:#0d9488; --chip:#f1f5f9; --quote:#f8fafc; }
@media (prefers-color-scheme: dark) {
:root { --fg:#e2e8f0; --muted:#94a3b8; --bg:#0f172a; --line:#334155;
--accent:#2dd4bf; --chip:#1e293b; --quote:#1e293b; }
}
* { box-sizing: border-box; }
body { margin: 0 auto; padding: 1.25rem 1rem 4rem; max-width: 46rem;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 17px; line-height: 1.55; color: var(--fg); background: var(--bg);
-webkit-text-size-adjust: 100%; }
nav { font-size: .85rem; margin-bottom: 1.25rem; }
nav a { color: var(--muted); }
h1 { font-size: 1.55rem; line-height: 1.25; margin: 0 0 .35rem; }
h2 { font-size: 1.2rem; margin: 2rem 0 .5rem; padding-top: .5rem;
border-top: 1px solid var(--line); }
h3 { font-size: 1.02rem; margin: 1.4rem 0 .4rem; }
p, ul, ol { margin: .6rem 0; }
ul, ol { padding-left: 1.3rem; }
li { margin: .25rem 0; }
li.task { list-style: none; margin-left: -1.3rem; }
li.task input { margin-right: .45rem; }
a { color: var(--accent); }
a.ext::after { content: " ↗"; font-size: .8em; color: var(--muted); }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em;
background: var(--chip); padding: .1em .35em; border-radius: 4px; }
pre { background: var(--chip); padding: .75rem; border-radius: 6px; overflow-x: auto; }
pre code { background: none; padding: 0; }
blockquote { margin: .9rem 0; padding: .6rem .9rem; background: var(--quote);
border-left: 3px solid var(--accent); border-radius: 0 6px 6px 0; }
blockquote p { margin: .2rem 0; }
hr { border: 0; border-top: 1px solid var(--line); margin: 1.5rem 0; }
.table-wrap { overflow-x: auto; margin: .8rem 0; -webkit-overflow-scrolling: touch; }
table { border-collapse: collapse; width: 100%; font-size: .92rem; }
th, td { border: 1px solid var(--line); padding: .4rem .55rem; vertical-align: top; }
th { background: var(--chip); font-weight: 600; }
.chip { display: inline-block; background: var(--chip); color: var(--muted);
font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
padding: .15rem .5rem; border-radius: 999px; }
.lead { color: var(--muted); margin: .35rem 0 1.25rem; }
.meta { margin: 0 0 1.5rem; }
.meta summary { cursor: pointer; font-size: .85rem; color: var(--muted);
padding: .3rem 0; }
.meta table { font-size: .85rem; }
.meta th { width: 38%; text-align: left; font-weight: 500; word-break: break-word; }
.cards { list-style: none; padding: 0; margin: 0; }
.cards li { border: 1px solid var(--line); border-radius: 8px; padding: .8rem .9rem;
margin: .6rem 0; }
.cards a { font-weight: 600; text-decoration: none; }
.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); }
</style>
</head>
<body>
<nav><a href="../index.html">← Wszystkie karty</a> · <a href="../../viz.html">Graf</a></nav>
<span class="chip">oferta</span>
<h1>AlpenParks Rehrenberg Viehhofen — PLAN C</h1>
<p class="lead">Najtańszy nowy obiekt na liście — przestronne apartamenty z prywatnymi saunami w Viehhofen, wariant rezerwowy o mieszanych opiniach, PLAN C.</p>
<details class="meta" open><summary>Metadane (frontmatter)</summary><div class="table-wrap"><table><tbody><tr><th>type</th><td>oferta</td></tr><tr><th>title</th><td>AlpenParks Apartments Rehrenberg</td></tr><tr><th>description</th><td>Najtańszy nowy obiekt na liście — przestronne apartamenty z prywatnymi saunami w Viehhofen, wariant rezerwowy o mieszanych opiniach, PLAN C.</td></tr><tr><th>timestamp</th><td>2026-07-31T00:00:00Z</td></tr><tr><th>tags</th><td>nocleg, viehhofen, plan-c</td></tr><tr><th>id</th><td>alpenparks-rehrenberg</td></tr><tr><th>nazwa</th><td>AlpenParks Apartments Rehrenberg</td></tr><tr><th>operator</th><td>AlpenParks</td></tr><tr><th>miejscowosc</th><td>Viehhofen</td></tr><tr><th>gps</th><td>47.3662, 12.7317</td></tr><tr><th>odleglosc_od_wyciagu</th><td>510 min pieszo do gondoli zellamseeXpress</td></tr><tr><th>ocena</th><td>4.2/5 (Google) — opinie mieszane</td></tr><tr><th>uklad_dla_grupy</th><td>4 osobne apartamenty</td></tr><tr><th>cena_za_apartament_tydzien</th><td>~1 1001 500 € (szacunek, styczeń poza feriami)</td></tr><tr><th>koszt_rodzina_2p2</th><td>~1 1001 500 €</td></tr><tr><th>koszt_rodzina_2p1</th><td>~9501 250 €</td></tr><tr><th>status_ceny</th><td>SZACUNEK — silnik online na alpenparks.at</td></tr><tr><th>priorytet</th><td>PLAN C (rezerwowy)</td></tr><tr><th>www</th><td><a href="https://www.alpenparks.at" target="_blank" rel="noopener" class="ext">https://www.alpenparks.at</a></td></tr><tr><th>ostatnia_aktualizacja</th><td>2026-07-31</td></tr></tbody></table></div></details>
<h2>W skrócie</h2>
<p>Najtańszy "nowy" obiekt na liście: przestronne, nowoczesne apartamenty z prywatnymi saunami w Viehhofen. Traktowany jako rezerwa — cena kusi, ale opinie są mieszane i logistyka na stok słabsza niż u konkurencji.</p>
<h2>Fakty</h2>
<ul><li>Nowe budownictwo, duże apartamenty, część z własną sauną.</li><li>510 min pieszo do gondoli zellamseeXpress (strona Zell am See); do Skicircusu skibusem.</li><li>Rezerwacja online przez alpenparks.at.</li></ul>
<h2>Układ dla naszej grupy (14 os.)</h2>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">Rodzina</th><th style="text-align:left">Szacunkowy koszt/tydzień</th></tr></thead><tbody><tr><td style="text-align:left">2× rodzina 2+2</td><td style="text-align:left">~1 1001 500 €</td></tr><tr><td style="text-align:left">2× rodzina 2+1</td><td style="text-align:left">~9501 250 €</td></tr></tbody></table></div>
<h2>Plusy / minusy</h2>
<p><strong>+</strong> najniższa cena za nowy standard; prywatne sauny; duża dostępność (spory obiekt); pełna rezerwacja online.</p>
<p><strong></strong> powtarzające się w recenzjach zastrzeżenia do skibusa (mały, zatłoczony rano) i obsługi/organizacji — <strong>przed rezerwacją przeczytać świeże opinie z zimy 25/26</strong>; Viehhofen bez wieczornego życia; sumarycznie gorszy pakiet niż Sportchalet za podobne pieniądze.</p>
<h2>Do zrobienia</h2>
<ul><li class="task"><input type="checkbox" disabled> Tylko jeśli plany A i B nie wypalą: sprawdzić ceny datami 1623.01.2027.</li><li class="task"><input type="checkbox" disabled> Zweryfikować najnowsze opinie (Booking/Google, filtr: zima 2026).</li></ul>
<footer>Wygenerowane z bundle'a OKF przez <code>gen_pages.py</code> — nie edytuj
tych plików, edytuj <code>.md</code> i przegeneruj.</footer>
</body>
</html>

View file

@ -0,0 +1,83 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AlpenParks Hotel &amp; Apartment Sonnleiten — Ski 2027 — Skicircus Saalbach</title>
<style>
:root { color-scheme: light dark; --fg:#0f172a; --muted:#64748b; --bg:#ffffff;
--line:#e2e8f0; --accent:#0d9488; --chip:#f1f5f9; --quote:#f8fafc; }
@media (prefers-color-scheme: dark) {
:root { --fg:#e2e8f0; --muted:#94a3b8; --bg:#0f172a; --line:#334155;
--accent:#2dd4bf; --chip:#1e293b; --quote:#1e293b; }
}
* { box-sizing: border-box; }
body { margin: 0 auto; padding: 1.25rem 1rem 4rem; max-width: 46rem;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 17px; line-height: 1.55; color: var(--fg); background: var(--bg);
-webkit-text-size-adjust: 100%; }
nav { font-size: .85rem; margin-bottom: 1.25rem; }
nav a { color: var(--muted); }
h1 { font-size: 1.55rem; line-height: 1.25; margin: 0 0 .35rem; }
h2 { font-size: 1.2rem; margin: 2rem 0 .5rem; padding-top: .5rem;
border-top: 1px solid var(--line); }
h3 { font-size: 1.02rem; margin: 1.4rem 0 .4rem; }
p, ul, ol { margin: .6rem 0; }
ul, ol { padding-left: 1.3rem; }
li { margin: .25rem 0; }
li.task { list-style: none; margin-left: -1.3rem; }
li.task input { margin-right: .45rem; }
a { color: var(--accent); }
a.ext::after { content: " ↗"; font-size: .8em; color: var(--muted); }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em;
background: var(--chip); padding: .1em .35em; border-radius: 4px; }
pre { background: var(--chip); padding: .75rem; border-radius: 6px; overflow-x: auto; }
pre code { background: none; padding: 0; }
blockquote { margin: .9rem 0; padding: .6rem .9rem; background: var(--quote);
border-left: 3px solid var(--accent); border-radius: 0 6px 6px 0; }
blockquote p { margin: .2rem 0; }
hr { border: 0; border-top: 1px solid var(--line); margin: 1.5rem 0; }
.table-wrap { overflow-x: auto; margin: .8rem 0; -webkit-overflow-scrolling: touch; }
table { border-collapse: collapse; width: 100%; font-size: .92rem; }
th, td { border: 1px solid var(--line); padding: .4rem .55rem; vertical-align: top; }
th { background: var(--chip); font-weight: 600; }
.chip { display: inline-block; background: var(--chip); color: var(--muted);
font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
padding: .15rem .5rem; border-radius: 999px; }
.lead { color: var(--muted); margin: .35rem 0 1.25rem; }
.meta { margin: 0 0 1.5rem; }
.meta summary { cursor: pointer; font-size: .85rem; color: var(--muted);
padding: .3rem 0; }
.meta table { font-size: .85rem; }
.meta th { width: 38%; text-align: left; font-weight: 500; word-break: break-word; }
.cards { list-style: none; padding: 0; margin: 0; }
.cards li { border: 1px solid var(--line); border-radius: 8px; padding: .8rem .9rem;
margin: .6rem 0; }
.cards a { font-weight: 600; text-decoration: none; }
.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); }
</style>
</head>
<body>
<nav><a href="../index.html">← Wszystkie karty</a> · <a href="../../viz.html">Graf</a></nav>
<span class="chip">oferta</span>
<h1>AlpenParks Sonnleiten Saalbach</h1>
<p class="lead">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.</p>
<details class="meta" open><summary>Metadane (frontmatter)</summary><div class="table-wrap"><table><tbody><tr><th>type</th><td>oferta</td></tr><tr><th>title</th><td>AlpenParks Hotel &amp; Apartment Sonnleiten</td></tr><tr><th>description</th><td>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.</td></tr><tr><th>timestamp</th><td>2026-07-31T00:00:00Z</td></tr><tr><th>tags</th><td>nocleg, saalbach, aparthotel</td></tr><tr><th>id</th><td>alpenparks-sonnleiten</td></tr><tr><th>nazwa</th><td>AlpenParks Hotel &amp; Apartment Sonnleiten</td></tr><tr><th>operator</th><td>AlpenParks</td></tr><tr><th>miejscowosc</th><td>Saalbach</td></tr><tr><th>gps</th><td>47.3953, 12.6378</td></tr><tr><th>odleglosc_od_wyciagu</th><td>~2 min pieszo do stoku</td></tr><tr><th>ocena</th><td>4.6/5 (Google)</td></tr><tr><th>uklad_dla_grupy</th><td>4 osobne apartamenty (po 1 na rodzinę)</td></tr><tr><th>cena_za_apartament_tydzien</th><td>~1 4001 900 € (szacunek, styczeń poza feriami)</td></tr><tr><th>koszt_rodzina_2p2</th><td>~1 4001 900 €</td></tr><tr><th>koszt_rodzina_2p1</th><td>~1 2001 500 €</td></tr><tr><th>status_ceny</th><td>SZACUNEK — silnik rezerwacyjny online na alpenparks.at</td></tr><tr><th>priorytet</th><td>PLAN A-bis (największa szansa na 4 wolne apartamenty)</td></tr><tr><th>www</th><td><a href="https://www.alpenparks.at" target="_blank" rel="noopener" class="ext">https://www.alpenparks.at</a></td></tr><tr><th>ostatnia_aktualizacja</th><td>2026-07-31</td></tr></tbody></table></div></details>
<h2>W skrócie</h2>
<p>Duży, nowoczesny aparthotel ~2 minuty pieszo od stoku w Saalbach. Kluczowa przewaga: <strong>skala obiektu</strong> — kilkadziesiąt apartamentów oznacza największe prawdopodobieństwo, że 4 jednostki będą wolne w jednym tygodniu, i pełną rezerwację online bez korespondencji.</p>
<h2>Fakty</h2>
<ul><li>Apartamenty rodzinne z aneksami kuchennymi, różne rozmiary (28 os.).</li><li><strong>Basen + sauna w cenie</strong> — argument nie do przecenienia przy 5 dzieciakach i tygodniu intensywnej jazdy.</li><li>Restauracja w obiekcie (opcja "nie gotujemy dziś").</li><li>Rezerwacja przez silnik online (EN), przejrzyste ceny od ręki.</li></ul>
<h2>Układ dla naszej grupy (14 os.)</h2>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">Rodzina</th><th style="text-align:left">Apartament</th><th style="text-align:left">Szacunkowy koszt/tydzień</th></tr></thead><tbody><tr><td style="text-align:left">2× rodzina 2+2</td><td style="text-align:left">4-os.</td><td style="text-align:left">~1 4001 900 €</td></tr><tr><td style="text-align:left">2× rodzina 2+1</td><td style="text-align:left">34-os.</td><td style="text-align:left">~1 2001 500 €</td></tr></tbody></table></div>
<h2>Plusy / minusy</h2>
<p><strong>+</strong> basen dla dzieci, restauracja, wysoka dostępność, przewidywalna jakość sieciówki, rezerwacja i anulacja na warunkach online.</p>
<p><strong></strong> mniej "kameralnie" niż u prywatnego gospodarza; ceny sieciówki sztywniejsze w negocjacji (choć mail grupowy i tak warto wysłać); lokalizacja dobra, ale nie tak spektakularna jak Kristall przy gondoli.</p>
<h2>Do zrobienia</h2>
<ul><li class="task"><input type="checkbox" disabled> Wpisać daty 1623.01.2027 w silnik na alpenparks.at → zrzut cen rzeczywistych dla 4 jednostek.</li><li class="task"><input type="checkbox" disabled> Równolegle mail grupowy z pytaniem o rabat przy 4 apartamentach.</li><li class="task"><input type="checkbox" disabled> Sprawdzić politykę anulacji (sieciówki często mają elastyczne taryfy za dopłatą — przy grupie 14 os. warto).</li></ul>
<footer>Wygenerowane z bundle'a OKF przez <code>gen_pages.py</code> — nie edytuj
tych plików, edytuj <code>.md</code> i przegeneruj.</footer>
</body>
</html>

View file

@ -0,0 +1,85 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Duży dom wakacyjny 1214 os. (do wyszukania) — Ski 2027 — Skicircus Saalbach</title>
<style>
:root { color-scheme: light dark; --fg:#0f172a; --muted:#64748b; --bg:#ffffff;
--line:#e2e8f0; --accent:#0d9488; --chip:#f1f5f9; --quote:#f8fafc; }
@media (prefers-color-scheme: dark) {
:root { --fg:#e2e8f0; --muted:#94a3b8; --bg:#0f172a; --line:#334155;
--accent:#2dd4bf; --chip:#1e293b; --quote:#1e293b; }
}
* { box-sizing: border-box; }
body { margin: 0 auto; padding: 1.25rem 1rem 4rem; max-width: 46rem;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 17px; line-height: 1.55; color: var(--fg); background: var(--bg);
-webkit-text-size-adjust: 100%; }
nav { font-size: .85rem; margin-bottom: 1.25rem; }
nav a { color: var(--muted); }
h1 { font-size: 1.55rem; line-height: 1.25; margin: 0 0 .35rem; }
h2 { font-size: 1.2rem; margin: 2rem 0 .5rem; padding-top: .5rem;
border-top: 1px solid var(--line); }
h3 { font-size: 1.02rem; margin: 1.4rem 0 .4rem; }
p, ul, ol { margin: .6rem 0; }
ul, ol { padding-left: 1.3rem; }
li { margin: .25rem 0; }
li.task { list-style: none; margin-left: -1.3rem; }
li.task input { margin-right: .45rem; }
a { color: var(--accent); }
a.ext::after { content: " ↗"; font-size: .8em; color: var(--muted); }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em;
background: var(--chip); padding: .1em .35em; border-radius: 4px; }
pre { background: var(--chip); padding: .75rem; border-radius: 6px; overflow-x: auto; }
pre code { background: none; padding: 0; }
blockquote { margin: .9rem 0; padding: .6rem .9rem; background: var(--quote);
border-left: 3px solid var(--accent); border-radius: 0 6px 6px 0; }
blockquote p { margin: .2rem 0; }
hr { border: 0; border-top: 1px solid var(--line); margin: 1.5rem 0; }
.table-wrap { overflow-x: auto; margin: .8rem 0; -webkit-overflow-scrolling: touch; }
table { border-collapse: collapse; width: 100%; font-size: .92rem; }
th, td { border: 1px solid var(--line); padding: .4rem .55rem; vertical-align: top; }
th { background: var(--chip); font-weight: 600; }
.chip { display: inline-block; background: var(--chip); color: var(--muted);
font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
padding: .15rem .5rem; border-radius: 999px; }
.lead { color: var(--muted); margin: .35rem 0 1.25rem; }
.meta { margin: 0 0 1.5rem; }
.meta summary { cursor: pointer; font-size: .85rem; color: var(--muted);
padding: .3rem 0; }
.meta table { font-size: .85rem; }
.meta th { width: 38%; text-align: left; font-weight: 500; word-break: break-word; }
.cards { list-style: none; padding: 0; margin: 0; }
.cards li { border: 1px solid var(--line); border-radius: 8px; padding: .8rem .9rem;
margin: .6rem 0; }
.cards a { font-weight: 600; text-decoration: none; }
.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); }
</style>
</head>
<body>
<nav><a href="../index.html">← Wszystkie karty</a> · <a href="../../viz.html">Graf</a></nav>
<span class="chip">oferta-koncept</span>
<h1>Dom dla całej grupy (1214 os.) — koncept</h1>
<p class="lead">Koncept wynajęcia jednego domu wakacyjnego dla całej grupy zamiast czterech osobnych apartamentów — klimat chaletowy w połowie ceny Glemm Lodge.</p>
<details class="meta" open><summary>Metadane (frontmatter)</summary><div class="table-wrap"><table><tbody><tr><th>type</th><td>oferta-koncept</td></tr><tr><th>title</th><td>Duży dom wakacyjny 1214 os. (do wyszukania)</td></tr><tr><th>description</th><td>Koncept wynajęcia jednego domu wakacyjnego dla całej grupy zamiast czterech osobnych apartamentów — klimat chaletowy w połowie ceny Glemm Lodge.</td></tr><tr><th>timestamp</th><td>2026-07-31T00:00:00Z</td></tr><tr><th>tags</th><td>nocleg, dom-wakacyjny, koncept</td></tr><tr><th>id</th><td>dom-dla-grupy</td></tr><tr><th>nazwa</th><td>Duży dom wakacyjny 1214 os. (do wyszukania)</td></tr><tr><th>miejscowosc</th><td>Viehhofen / Maishofen / strona Fieberbrunn</td></tr><tr><th>uklad_dla_grupy</th><td>jeden dom dla całej grupy</td></tr><tr><th>cena_za_calosc_tydzien</th><td>4 5006 500 € (szacunek, styczeń poza feriami)</td></tr><tr><th>koszt_rodzina_2p2</th><td>~1 4001 900 € (podział od osoby)</td></tr><tr><th>koszt_rodzina_2p1</th><td>~1 1001 500 € (podział od osoby)</td></tr><tr><th>status_ceny</th><td>SZACUNEK — konkretny dom do znalezienia</td></tr><tr><th>priorytet</th><td>WARIANT RÓWNOLEGŁY (klimat 'wszyscy razem')</td></tr><tr><th>gdzie_szukac</th><td><a href="https://casamundo.pl" target="_blank" rel="noopener" class="ext">casamundo.pl</a>, <a href="https://interhome.pl" target="_blank" rel="noopener" class="ext">interhome.pl</a>, <a href="https://e-domizil.de" target="_blank" rel="noopener" class="ext">e-domizil.de</a>, <a href="https://vrbo.com" target="_blank" rel="noopener" class="ext">vrbo.com</a>, <a href="https://belvilla.com" target="_blank" rel="noopener" class="ext">belvilla.com</a>, <a href="https://allchalets.com" target="_blank" rel="noopener" class="ext">allchalets.com</a></td></tr><tr><th>ostatnia_aktualizacja</th><td>2026-07-31</td></tr></tbody></table></div></details>
<h2>W skrócie</h2>
<p>Alternatywa dla "4 osobnych apartamentów": jeden duży dom wakacyjny dla całej czternastki. Klimat chaletowy jak w Glemm Lodge (wspólne kolacje, wieczory, dzieciaki razem), ale w budżecie ~połowy jego ceny — pod warunkiem odpuszczenia adresu w samym Saalbach.</p>
<h2>Gdzie geograficznie</h2>
<ul><li><strong>Viehhofen / Maishofen</strong> — 38 km od Saalbach, skibus lub 10 min autem.</li><li><strong>Strona Fieberbrunn / St. Jakob in Haus / Hochfilzen</strong> — wejście do Skicircusu od tyłu przez gondolę Fieberbrunn; okolica tańsza, spokojna.</li></ul>
<h2>Parametry do filtrowania przy szukaniu</h2>
<ul><li>≥ 5 sypialni (4 rodzicielskie + konfiguracje dla 6 dzieci), ≥ 3 łazienki.</li><li>Duży salon + jadalnia na 14 os. (wspólne kolacje to sens tego wariantu).</li><li>Parking na 4 auta, narciarnia/suszarnia.</li><li>Cena docelowa: ≤ 6 500 €/tydzień w terminie 1623.01.2027.</li><li>Sauna = miły bonus, częsty w tym segmencie.</li></ul>
<h2>Rozliczenie (podział od osoby, 14 os.)</h2>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">Rodzina</th><th style="text-align:left">Udział</th><th style="text-align:left">Przy 5 500 €/tydzień</th></tr></thead><tbody><tr><td style="text-align:left">2+2</td><td style="text-align:left">4/14</td><td style="text-align:left">~1 570 €</td></tr><tr><td style="text-align:left">2+1</td><td style="text-align:left">3/14</td><td style="text-align:left">~1 180 €</td></tr></tbody></table></div>
<h2>Plusy / minusy</h2>
<p><strong>+</strong> najlepszy klimat grupowy; wspólne gotowanie tnie koszty jedzenia; zwykle najlepszy metraż za euro; jedna rezerwacja.</p>
<p><strong></strong> logistyka poranna (14 os. na skibus/auta); jedna kuchnia i salon = potrzebna zgodność grupy; pojedyncza rezerwacja = pojedyncze ryzyko (anulacja jednej rodziny obciąża resztę — spisać zasady z góry!); konkretny dom trzeba dopiero znaleźć.</p>
<h2>Do zrobienia</h2>
<ul><li class="task"><input type="checkbox" disabled> Przeszukać portale (lista w nagłówku) filtrem: 14 os., 1623.01.2027, Viehhofen/Maishofen/Fieberbrunn.</li><li class="task"><input type="checkbox" disabled> Shortlista 3 domów → weryfikacja opinii → zapytania.</li><li class="task"><input type="checkbox" disabled> Umowa wewnętrzna grupy: zaliczki, zasady rezygnacji, podział od osoby.</li></ul>
<footer>Wygenerowane z bundle'a OKF przez <code>gen_pages.py</code> — nie edytuj
tych plików, edytuj <code>.md</code> i przegeneruj.</footer>
</body>
</html>

View file

@ -0,0 +1,88 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Glemm Lodge Apartments ⭐⭐⭐⭐ — Ski 2027 — Skicircus Saalbach</title>
<style>
:root { color-scheme: light dark; --fg:#0f172a; --muted:#64748b; --bg:#ffffff;
--line:#e2e8f0; --accent:#0d9488; --chip:#f1f5f9; --quote:#f8fafc; }
@media (prefers-color-scheme: dark) {
:root { --fg:#e2e8f0; --muted:#94a3b8; --bg:#0f172a; --line:#334155;
--accent:#2dd4bf; --chip:#1e293b; --quote:#1e293b; }
}
* { box-sizing: border-box; }
body { margin: 0 auto; padding: 1.25rem 1rem 4rem; max-width: 46rem;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 17px; line-height: 1.55; color: var(--fg); background: var(--bg);
-webkit-text-size-adjust: 100%; }
nav { font-size: .85rem; margin-bottom: 1.25rem; }
nav a { color: var(--muted); }
h1 { font-size: 1.55rem; line-height: 1.25; margin: 0 0 .35rem; }
h2 { font-size: 1.2rem; margin: 2rem 0 .5rem; padding-top: .5rem;
border-top: 1px solid var(--line); }
h3 { font-size: 1.02rem; margin: 1.4rem 0 .4rem; }
p, ul, ol { margin: .6rem 0; }
ul, ol { padding-left: 1.3rem; }
li { margin: .25rem 0; }
li.task { list-style: none; margin-left: -1.3rem; }
li.task input { margin-right: .45rem; }
a { color: var(--accent); }
a.ext::after { content: " ↗"; font-size: .8em; color: var(--muted); }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em;
background: var(--chip); padding: .1em .35em; border-radius: 4px; }
pre { background: var(--chip); padding: .75rem; border-radius: 6px; overflow-x: auto; }
pre code { background: none; padding: 0; }
blockquote { margin: .9rem 0; padding: .6rem .9rem; background: var(--quote);
border-left: 3px solid var(--accent); border-radius: 0 6px 6px 0; }
blockquote p { margin: .2rem 0; }
hr { border: 0; border-top: 1px solid var(--line); margin: 1.5rem 0; }
.table-wrap { overflow-x: auto; margin: .8rem 0; -webkit-overflow-scrolling: touch; }
table { border-collapse: collapse; width: 100%; font-size: .92rem; }
th, td { border: 1px solid var(--line); padding: .4rem .55rem; vertical-align: top; }
th { background: var(--chip); font-weight: 600; }
.chip { display: inline-block; background: var(--chip); color: var(--muted);
font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
padding: .15rem .5rem; border-radius: 999px; }
.lead { color: var(--muted); margin: .35rem 0 1.25rem; }
.meta { margin: 0 0 1.5rem; }
.meta summary { cursor: pointer; font-size: .85rem; color: var(--muted);
padding: .3rem 0; }
.meta table { font-size: .85rem; }
.meta th { width: 38%; text-align: left; font-weight: 500; word-break: break-word; }
.cards { list-style: none; padding: 0; margin: 0; }
.cards li { border: 1px solid var(--line); border-radius: 8px; padding: .8rem .9rem;
margin: .6rem 0; }
.cards a { font-weight: 600; text-decoration: none; }
.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); }
</style>
</head>
<body>
<nav><a href="../index.html">← Wszystkie karty</a> · <a href="../../viz.html">Graf</a></nav>
<span class="chip">oferta</span>
<h1>Glemm Lodge Apartments — poza budżetem (dane referencyjne)</h1>
<p class="lead">Jedyna oferta z w pełni zweryfikowanym cennikiem (~4 370 €/rodzinę) — odrzucona budżetowo, służy jako punkt odniesienia cenowego.</p>
<details class="meta" open><summary>Metadane (frontmatter)</summary><div class="table-wrap"><table><tbody><tr><th>type</th><td>oferta</td></tr><tr><th>title</th><td>Glemm Lodge Apartments ⭐⭐⭐⭐</td></tr><tr><th>description</th><td>Jedyna oferta z w pełni zweryfikowanym cennikiem (~4 370 €/rodzinę) — odrzucona budżetowo, służy jako punkt odniesienia cenowego.</td></tr><tr><th>timestamp</th><td>2026-07-31T00:00:00Z</td></tr><tr><th>tags</th><td>nocleg, saalbach, referencja</td></tr><tr><th>id</th><td>glemm-lodge</td></tr><tr><th>nazwa</th><td>Glemm Lodge Apartments ⭐⭐⭐⭐</td></tr><tr><th>operator</th><td>Dieter Weschenfelder (prywatny)</td></tr><tr><th>kontakt</th><td><a href="mailto:info@glemmlodge-apartments.com">info@glemmlodge-apartments.com</a></td></tr><tr><th>miejscowosc</th><td>między Saalbach a Hinterglemm</td></tr><tr><th>adres</th><td>Glemmtaler Landesstr. 263, 5753 Saalbach</td></tr><tr><th>gps</th><td>47.3879, 12.6178</td></tr><tr><th>odleglosc_od_wyciagu</th><td>zjazd trasą 86 pod dom; skibus pod drzwiami (5 min do gondoli)</td></tr><tr><th>ocena</th><td>9.1/10 (Booking), 4.8/5 (Google)</td></tr><tr><th>uklad_dla_grupy</th><td>2 apartamenty po 7 os. (Top 2 + Top 3)</td></tr><tr><th>cena_za_apartament_tydzien</th><td>7 000 € + 500 € sprzątanie + taksy (CENNIK OFICJALNY 26/27)</td></tr><tr><th>koszt_rodzina_2p2</th><td>~4 370 €</td></tr><tr><th>koszt_rodzina_2p1</th><td>~3 277 €</td></tr><tr><th>status_ceny</th><td>ZWERYFIKOWANE (cennik + kalendarz obiektu, stan 31.07.2026)</td></tr><tr><th>priorytet</th><td>ODRZUCONA BUDŻETOWO (wraca do gry przy rabacie do ~5 000 €/apartament)</td></tr><tr><th>www</th><td><a href="https://glemmlodge-apartments.com" target="_blank" rel="noopener" class="ext">https://glemmlodge-apartments.com</a></td></tr><tr><th>ostatnia_aktualizacja</th><td>2026-07-31</td></tr></tbody></table></div></details>
<h2>Dlaczego w bazie mimo odrzucenia</h2>
<p>Jedyny obiekt z <strong>pełnymi zweryfikowanymi danymi</strong> (publiczny cennik i kalendarz obłożenia) — służy jako punkt odniesienia cenowego. Wraca do gry, jeśli właściciel zejdzie do ~5 000 €/apartament przy 7 osobach (precedens: cennik letni skaluje ceny wg liczby osób).</p>
<h2>Fakty (zweryfikowane 31.07.2026)</h2>
<ul><li>3 luksusowe apartamenty 1012 os. (117128 m², po 3 łazienki + WC).</li><li>Zjazd trasą 86 bezpośrednio pod dom; powrót na stok skibusem (przystanek pod drzwiami, 5 min); supermarket MPREIS naprzeciwko.</li><li>Wallbox dla aut elektrycznych; zakaz imprez wieczorów kawalerskich itp.</li></ul>
<h2>Cennik zima 2026/27 (oficjalny)</h2>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">Okres</th><th style="text-align:left">Sezon</th><th style="text-align:left">Za noc</th><th style="text-align:left">Za tydzień</th></tr></thead><tbody><tr><td style="text-align:left">27.1111.12.2026</td><td style="text-align:left">przedsezon</td><td style="text-align:left">800 €</td><td style="text-align:left">5 600 €</td></tr><tr><td style="text-align:left">1218.12.2026</td><td style="text-align:left">niski</td><td style="text-align:left">900 €</td><td style="text-align:left">6 300 €</td></tr><tr><td style="text-align:left">1925.12.2026</td><td style="text-align:left">wysoki</td><td style="text-align:left">1 000 €</td><td style="text-align:left">7 000 €</td></tr><tr><td style="text-align:left">26.12.202601.01.2027</td><td style="text-align:left">top</td><td style="text-align:left">1 200 €</td><td style="text-align:left">8 400 €</td></tr><tr><td style="text-align:left"><strong>02.0105.03.2027</strong></td><td style="text-align:left"><strong>wysoki</strong></td><td style="text-align:left"><strong>1 000 €</strong></td><td style="text-align:left"><strong>7 000 €</strong></td></tr><tr><td style="text-align:left">0619.03.2027</td><td style="text-align:left">niski</td><td style="text-align:left">900 €</td><td style="text-align:left">6 300 €</td></tr><tr><td style="text-align:left">20.0304.04.2027</td><td style="text-align:left">późny</td><td style="text-align:left">800 €</td><td style="text-align:left">5 600 €</td></tr></tbody></table></div>
<p>Dodatkowo: sprzątanie końcowe 500 €/apartament, taksa 3 €/os./noc, kaucja zwrotna 900 €/apartament.</p>
<h2>Dostępność w naszym oknie (stan 31.07.2026)</h2>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">Termin</th><th style="text-align:left">Top 1</th><th style="text-align:left">Top 2</th><th style="text-align:left">Top 3</th><th style="text-align:left">2 apartamenty naraz?</th></tr></thead><tbody><tr><td style="text-align:left">29.01.2027</td><td style="text-align:left">zajęty</td><td style="text-align:left">WOLNY</td><td style="text-align:left">zajęty</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">916.01.2027</td><td style="text-align:left">zajęty</td><td style="text-align:left">zajęty</td><td style="text-align:left">WOLNY</td><td style="text-align:left"></td></tr><tr><td style="text-align:left"><strong>1623.01.2027</strong></td><td style="text-align:left">WOLNY</td><td style="text-align:left">WOLNY</td><td style="text-align:left">WOLNY</td><td style="text-align:left"></td></tr></tbody></table></div>
<h2>Kosztorys wariantu 2× apartament, 1623.01 (14 os.)</h2>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">Pozycja</th><th style="text-align:left">Na apartament (7 os.)</th></tr></thead><tbody><tr><td style="text-align:left">Czynsz 7 nocy</td><td style="text-align:left">7 000 €</td></tr><tr><td style="text-align:left">Sprzątanie</td><td style="text-align:left">500 €</td></tr><tr><td style="text-align:left">Taksa (3 € × 7 os. × 7 nocy)</td><td style="text-align:left">147 €</td></tr><tr><td style="text-align:left"><strong>Razem</strong> (bez kaucji 900 €)</td><td style="text-align:left"><strong>7 647 €</strong></td></tr></tbody></table></div>
<p>Podział od osoby: rodzina 2+2 → ~4 370 €, rodzina 2+1 → ~3 277 €. <strong>Wniosek: 2× powyżej celu budżetowego (~2 000 €/rodzinę).</strong></p>
<h2>Warunki rezerwacji</h2>
<ul><li>Zaliczka 30% od razu; reszta + kaucja 90 dni przed przyjazdem.</li><li><strong>Darmowa anulacja tylko do 91 dni przed</strong> (dla 16.01 → deadline ~16.10.2026); później 100% kosztów.</li></ul>
<h2>Do zrobienia (opcjonalnie)</h2>
<ul><li class="task"><input type="checkbox" disabled> Mail do Dietera: cena za 7 os./apartament zamiast 1012 — jeśli ≤ 5 000 €, wariant wraca do rozważań (~2 900/2 200 € na rodzinę).</li></ul>
<footer>Wygenerowane z bundle'a OKF przez <code>gen_pages.py</code> — nie edytuj
tych plików, edytuj <code>.md</code> i przegeneruj.</footer>
</body>
</html>

View file

@ -0,0 +1,83 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Appartementhaus Kristall at SchattbergXpress — Ski 2027 — Skicircus Saalbach</title>
<style>
:root { color-scheme: light dark; --fg:#0f172a; --muted:#64748b; --bg:#ffffff;
--line:#e2e8f0; --accent:#0d9488; --chip:#f1f5f9; --quote:#f8fafc; }
@media (prefers-color-scheme: dark) {
:root { --fg:#e2e8f0; --muted:#94a3b8; --bg:#0f172a; --line:#334155;
--accent:#2dd4bf; --chip:#1e293b; --quote:#1e293b; }
}
* { box-sizing: border-box; }
body { margin: 0 auto; padding: 1.25rem 1rem 4rem; max-width: 46rem;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 17px; line-height: 1.55; color: var(--fg); background: var(--bg);
-webkit-text-size-adjust: 100%; }
nav { font-size: .85rem; margin-bottom: 1.25rem; }
nav a { color: var(--muted); }
h1 { font-size: 1.55rem; line-height: 1.25; margin: 0 0 .35rem; }
h2 { font-size: 1.2rem; margin: 2rem 0 .5rem; padding-top: .5rem;
border-top: 1px solid var(--line); }
h3 { font-size: 1.02rem; margin: 1.4rem 0 .4rem; }
p, ul, ol { margin: .6rem 0; }
ul, ol { padding-left: 1.3rem; }
li { margin: .25rem 0; }
li.task { list-style: none; margin-left: -1.3rem; }
li.task input { margin-right: .45rem; }
a { color: var(--accent); }
a.ext::after { content: " ↗"; font-size: .8em; color: var(--muted); }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em;
background: var(--chip); padding: .1em .35em; border-radius: 4px; }
pre { background: var(--chip); padding: .75rem; border-radius: 6px; overflow-x: auto; }
pre code { background: none; padding: 0; }
blockquote { margin: .9rem 0; padding: .6rem .9rem; background: var(--quote);
border-left: 3px solid var(--accent); border-radius: 0 6px 6px 0; }
blockquote p { margin: .2rem 0; }
hr { border: 0; border-top: 1px solid var(--line); margin: 1.5rem 0; }
.table-wrap { overflow-x: auto; margin: .8rem 0; -webkit-overflow-scrolling: touch; }
table { border-collapse: collapse; width: 100%; font-size: .92rem; }
th, td { border: 1px solid var(--line); padding: .4rem .55rem; vertical-align: top; }
th { background: var(--chip); font-weight: 600; }
.chip { display: inline-block; background: var(--chip); color: var(--muted);
font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
padding: .15rem .5rem; border-radius: 999px; }
.lead { color: var(--muted); margin: .35rem 0 1.25rem; }
.meta { margin: 0 0 1.5rem; }
.meta summary { cursor: pointer; font-size: .85rem; color: var(--muted);
padding: .3rem 0; }
.meta table { font-size: .85rem; }
.meta th { width: 38%; text-align: left; font-weight: 500; word-break: break-word; }
.cards { list-style: none; padding: 0; margin: 0; }
.cards li { border: 1px solid var(--line); border-radius: 8px; padding: .8rem .9rem;
margin: .6rem 0; }
.cards a { font-weight: 600; text-decoration: none; }
.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); }
</style>
</head>
<body>
<nav><a href="../index.html">← Wszystkie karty</a> · <a href="../../viz.html">Graf</a></nav>
<span class="chip">oferta</span>
<h1>Appartementhaus Kristall at SchattbergXpress — PLAN A</h1>
<p class="lead">Apartamenty 50 m od gondoli Schattberg X-press w centrum Saalbach — najlepsza lokalizacja w rozważanym budżecie, PLAN A.</p>
<details class="meta" open><summary>Metadane (frontmatter)</summary><div class="table-wrap"><table><tbody><tr><th>type</th><td>oferta</td></tr><tr><th>title</th><td>Appartementhaus Kristall at SchattbergXpress</td></tr><tr><th>description</th><td>Apartamenty 50 m od gondoli Schattberg X-press w centrum Saalbach — najlepsza lokalizacja w rozważanym budżecie, PLAN A.</td></tr><tr><th>timestamp</th><td>2026-07-31T00:00:00Z</td></tr><tr><th>tags</th><td>nocleg, saalbach, plan-a</td></tr><tr><th>id</th><td>kristall-schattbergxpress</td></tr><tr><th>nazwa</th><td>Appartementhaus Kristall at SchattbergXpress</td></tr><tr><th>operator</th><td>All in One Apartments</td></tr><tr><th>miejscowosc</th><td>Saalbach (centrum)</td></tr><tr><th>adres</th><td>Schulstraße 341, 5753 Saalbach</td></tr><tr><th>gps</th><td>47.3903, 12.6355</td></tr><tr><th>odleglosc_od_wyciagu</th><td>50 m — gondola Schattberg X-press</td></tr><tr><th>ocena</th><td>8.5/10 (Booking)</td></tr><tr><th>uklad_dla_grupy</th><td>4 osobne apartamenty (po 1 na rodzinę)</td></tr><tr><th>cena_za_apartament_tydzien</th><td>~1 5002 000 € (szacunek, styczeń poza feriami)</td></tr><tr><th>koszt_rodzina_2p2</th><td>~1 5002 000 €</td></tr><tr><th>koszt_rodzina_2p1</th><td>~1 2001 600 €</td></tr><tr><th>status_ceny</th><td>SZACUNEK — do potwierdzenia zapytaniem</td></tr><tr><th>priorytet</th><td>PLAN A</td></tr><tr><th>www</th><td><a href="https://www.booking.com/hotel/at/appartementhaus-kristall.html" target="_blank" rel="noopener" class="ext">https://www.booking.com/hotel/at/appartementhaus-kristall.html</a></td></tr><tr><th>ostatnia_aktualizacja</th><td>2026-07-31</td></tr></tbody></table></div></details>
<h2>W skrócie</h2>
<p>Najlepsza lokalizacja w rozważanym budżecie: <strong>50 m od gondoli Schattberg X-press</strong> — głównej stacji startowej całego Skicircusu, w ścisłym centrum Saalbach. Nastolatki wychodzą z narciarni prosto na gondolę, zero skibusa, zero odwożenia. Rodzice mają restauracje, bary i sklep w promieniu 50 m.</p>
<h2>Fakty</h2>
<ul><li>Eleganckie apartamenty w stylu alpejskim, w pełni wyposażone aneksy kuchenne, balkony z widokiem na góry.</li><li>Apartamenty typowo 26 os.; opinie gości potwierdzają wygodne jednostki 4-osobowe <strong>z dwiema łazienkami</strong>.</li><li>1 bezpłatne miejsce parkingowe na apartament (ważne: 4 rodziny = 4 auta ✓).</li><li>Wspólna strefa spa w budynku; narciarnia z suszarką do butów.</li><li>Restauracja 20 m, sklep spożywczy 50 m, przystanek skibusa 10 m.</li></ul>
<h2>Układ dla naszej grupy (14 os.)</h2>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">Rodzina</th><th style="text-align:left">Apartament</th><th style="text-align:left">Szacunkowy koszt/tydzień</th></tr></thead><tbody><tr><td style="text-align:left">Rodzina A (2+2)</td><td style="text-align:left">4-os. z 2 łazienkami</td><td style="text-align:left">~1 5002 000 €</td></tr><tr><td style="text-align:left">Rodzina B (2+2)</td><td style="text-align:left">4-os. z 2 łazienkami</td><td style="text-align:left">~1 5002 000 €</td></tr><tr><td style="text-align:left">Rodzina C (2+1)</td><td style="text-align:left">34-os. (mniejszy)</td><td style="text-align:left">~1 2001 600 €</td></tr><tr><td style="text-align:left">Rodzina D (2+1)</td><td style="text-align:left">34-os. (mniejszy)</td><td style="text-align:left">~1 2001 600 €</td></tr></tbody></table></div>
<h2>Plusy / minusy</h2>
<p><strong>+</strong> lokalizacja bezkonkurencyjna (za taki adres w Saalbach zwykle się dopłaca), każda rodzina ma prywatność i własną łazienkę, spa w budynku, parking w cenie, centrum = wieczorne życie dla nastolatków pod nosem.</p>
<p><strong></strong> brak wspólnego dużego salonu na wieczory całej ekipy (wieczory "na apartamentach" albo w knajpie); dostępność 4 jednostek naraz do potwierdzenia; centrum Saalbach bywa głośne w weekendy.</p>
<h2>Do zrobienia</h2>
<ul><li class="task"><input type="checkbox" disabled> Sprawdzić dostępność 4 apartamentów: 1623.01.2027 (opcja B: 916.01) — Booking + strona All in One Apartments.</li><li class="task"><input type="checkbox" disabled> Wysłać zapytanie grupowe mailem (4 jednostki, 14 os., prośba o cenę grupową — przy 4 apartamentach rabat direct jest normą).</li><li class="task"><input type="checkbox" disabled> Dopytać: wysokość kaucji, sprzątanie końcowe, taksa, polityka anulacji.</li></ul>
<footer>Wygenerowane z bundle'a OKF przez <code>gen_pages.py</code> — nie edytuj
tych plików, edytuj <code>.md</code> i przegeneruj.</footer>
</body>
</html>

View file

@ -0,0 +1,84 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sportchalet Viehhofen — Ski 2027 — Skicircus Saalbach</title>
<style>
:root { color-scheme: light dark; --fg:#0f172a; --muted:#64748b; --bg:#ffffff;
--line:#e2e8f0; --accent:#0d9488; --chip:#f1f5f9; --quote:#f8fafc; }
@media (prefers-color-scheme: dark) {
:root { --fg:#e2e8f0; --muted:#94a3b8; --bg:#0f172a; --line:#334155;
--accent:#2dd4bf; --chip:#1e293b; --quote:#1e293b; }
}
* { box-sizing: border-box; }
body { margin: 0 auto; padding: 1.25rem 1rem 4rem; max-width: 46rem;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 17px; line-height: 1.55; color: var(--fg); background: var(--bg);
-webkit-text-size-adjust: 100%; }
nav { font-size: .85rem; margin-bottom: 1.25rem; }
nav a { color: var(--muted); }
h1 { font-size: 1.55rem; line-height: 1.25; margin: 0 0 .35rem; }
h2 { font-size: 1.2rem; margin: 2rem 0 .5rem; padding-top: .5rem;
border-top: 1px solid var(--line); }
h3 { font-size: 1.02rem; margin: 1.4rem 0 .4rem; }
p, ul, ol { margin: .6rem 0; }
ul, ol { padding-left: 1.3rem; }
li { margin: .25rem 0; }
li.task { list-style: none; margin-left: -1.3rem; }
li.task input { margin-right: .45rem; }
a { color: var(--accent); }
a.ext::after { content: " ↗"; font-size: .8em; color: var(--muted); }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em;
background: var(--chip); padding: .1em .35em; border-radius: 4px; }
pre { background: var(--chip); padding: .75rem; border-radius: 6px; overflow-x: auto; }
pre code { background: none; padding: 0; }
blockquote { margin: .9rem 0; padding: .6rem .9rem; background: var(--quote);
border-left: 3px solid var(--accent); border-radius: 0 6px 6px 0; }
blockquote p { margin: .2rem 0; }
hr { border: 0; border-top: 1px solid var(--line); margin: 1.5rem 0; }
.table-wrap { overflow-x: auto; margin: .8rem 0; -webkit-overflow-scrolling: touch; }
table { border-collapse: collapse; width: 100%; font-size: .92rem; }
th, td { border: 1px solid var(--line); padding: .4rem .55rem; vertical-align: top; }
th { background: var(--chip); font-weight: 600; }
.chip { display: inline-block; background: var(--chip); color: var(--muted);
font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
padding: .15rem .5rem; border-radius: 999px; }
.lead { color: var(--muted); margin: .35rem 0 1.25rem; }
.meta { margin: 0 0 1.5rem; }
.meta summary { cursor: pointer; font-size: .85rem; color: var(--muted);
padding: .3rem 0; }
.meta table { font-size: .85rem; }
.meta th { width: 38%; text-align: left; font-weight: 500; word-break: break-word; }
.cards { list-style: none; padding: 0; margin: 0; }
.cards li { border: 1px solid var(--line); border-radius: 8px; padding: .8rem .9rem;
margin: .6rem 0; }
.cards a { font-weight: 600; text-decoration: none; }
.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); }
</style>
</head>
<body>
<nav><a href="../index.html">← Wszystkie karty</a> · <a href="../../viz.html">Graf</a></nav>
<span class="chip">oferta</span>
<h1>Sportchalet Viehhofen — PLAN B (budżetowy faworyt)</h1>
<p class="lead">Kameralny obiekt holenderskich gospodarzy w Viehhofen z oceną 5.0 — najlepszy stosunek ceny do jakości, PLAN B.</p>
<details class="meta" open><summary>Metadane (frontmatter)</summary><div class="table-wrap"><table><tbody><tr><th>type</th><td>oferta</td></tr><tr><th>title</th><td>Sportchalet Viehhofen</td></tr><tr><th>description</th><td>Kameralny obiekt holenderskich gospodarzy w Viehhofen z oceną 5.0 — najlepszy stosunek ceny do jakości, PLAN B.</td></tr><tr><th>timestamp</th><td>2026-07-31T00:00:00Z</td></tr><tr><th>tags</th><td>nocleg, viehhofen, plan-b</td></tr><tr><th>id</th><td>sportchalet-viehhofen</td></tr><tr><th>nazwa</th><td>Sportchalet Viehhofen</td></tr><tr><th>operator</th><td>prywatni gospodarze (NL, komunikacja EN)</td></tr><tr><th>miejscowosc</th><td>Viehhofen (3 km od Saalbach)</td></tr><tr><th>gps</th><td>47.3678, 12.7351</td></tr><tr><th>odleglosc_od_wyciagu</th><td>gondola zellamseeXpress za rogiem; skibus do Skicircusu pod domem</td></tr><tr><th>ocena</th><td>5.0/5 (Google)</td></tr><tr><th>uklad_dla_grupy</th><td>34 apartamenty w jednym budynku</td></tr><tr><th>cena_za_apartament_tydzien</th><td>~1 2001 600 € (szacunek, styczeń poza feriami)</td></tr><tr><th>koszt_rodzina_2p2</th><td>~1 2001 600 €</td></tr><tr><th>koszt_rodzina_2p1</th><td>~1 0001 300 €</td></tr><tr><th>status_ceny</th><td>SZACUNEK — wycena tylko mailowo</td></tr><tr><th>priorytet</th><td>PLAN B (najlepszy stosunek ceny do jakości)</td></tr><tr><th>www</th><td><a href="https://sportchaletviehhofen.com" target="_blank" rel="noopener" class="ext">https://sportchaletviehhofen.com</a></td></tr><tr><th>ostatnia_aktualizacja</th><td>2026-07-31</td></tr></tbody></table></div></details>
<h2>W skrócie</h2>
<p>Kameralny obiekt w Viehhofen — wsi 3 km przed Saalbach — prowadzony przez holenderskich gospodarzy z oceną 5.0. Opinie gości wprost opisują scenariusz "kilka rodzin bookuje kilka apartamentów razem" — czyli dokładnie nasz przypadek. Najniższe ceny na liście przy bardzo dobrej jakości.</p>
<h2>Fakty</h2>
<ul><li>Kilka apartamentów w jednym budynku — możliwość zajęcia obiektu niemal na wyłączność przez naszą grupę.</li><li>Gondola <strong>zellamseeXpress</strong> za rogiem (strona Zell am See — na tym samym karnecie ALPIN CARD!), skibus do gondoli Schönleiten w Saalbach staje pod domem (bezpłatny ze skipassem, kursuje często).</li><li>Gospodarze mówią po angielsku, nastawieni rodzinnie, kontakt bezpośredni.</li></ul>
<h2>Układ dla naszej grupy (14 os.)</h2>
<p>Zależnie od konfiguracji apartamentów (do potwierdzenia mailem): wariant 4×(34 os.) albo 2×(67 os.) + 1×(34 os.). Przy zajęciu całego budynku wspólne wieczory de facto "jak w chalecie".</p>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">Rodzina</th><th style="text-align:left">Szacunkowy koszt/tydzień</th></tr></thead><tbody><tr><td style="text-align:left">2+2</td><td style="text-align:left">~1 2001 600 €</td></tr><tr><td style="text-align:left">2+1</td><td style="text-align:left">~1 0001 300 €</td></tr></tbody></table></div>
<h2>Plusy / minusy</h2>
<p><strong>+</strong> najniższa cena przy oceny 5.0; klimat "cały dom dla nas"; elastyczni prywatni gospodarze = realna negocjacja grupowa; parking bez problemu; dwie opcje startu na stok (Zell am See gondolą / Skicircus skibusem) — fajna różnorodność w trakcie tygodnia.</p>
<p><strong></strong> to nie ski-in/ski-out: do Skicircusu 510 min skibusem (z 5 dzieci i sprzętem — logistyka poranna do ogarnięcia); Viehhofen jest cichy — zero wieczornego życia dla nastolatków (trzeba dojechać do Saalbach); brak silnika online — wszystko mailowo.</p>
<h2>Do zrobienia</h2>
<ul><li class="task"><input type="checkbox" disabled> Mail z zapytaniem o dostępność wszystkich apartamentów 1623.01.2027 (opcja B: 916.01) + konfigurację łóżek dla 4+4+3+3.</li><li class="task"><input type="checkbox" disabled> Zapytać o cenę za zajęcie całego obiektu (package deal).</li><li class="task"><input type="checkbox" disabled> Potwierdzić rozkład skibusa w styczniu (częstotliwość rano 8:309:30).</li></ul>
<footer>Wygenerowane z bundle'a OKF przez <code>gen_pages.py</code> — nie edytuj
tych plików, edytuj <code>.md</code> i przegeneruj.</footer>
</body>
</html>

92
pages/region.html Normal file
View file

@ -0,0 +1,92 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Skicircus Saalbach-Hinterglemm-Leogang-Fieberbrunn — Ski 2027 — Skicircus Saalbach</title>
<style>
:root { color-scheme: light dark; --fg:#0f172a; --muted:#64748b; --bg:#ffffff;
--line:#e2e8f0; --accent:#0d9488; --chip:#f1f5f9; --quote:#f8fafc; }
@media (prefers-color-scheme: dark) {
:root { --fg:#e2e8f0; --muted:#94a3b8; --bg:#0f172a; --line:#334155;
--accent:#2dd4bf; --chip:#1e293b; --quote:#1e293b; }
}
* { box-sizing: border-box; }
body { margin: 0 auto; padding: 1.25rem 1rem 4rem; max-width: 46rem;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 17px; line-height: 1.55; color: var(--fg); background: var(--bg);
-webkit-text-size-adjust: 100%; }
nav { font-size: .85rem; margin-bottom: 1.25rem; }
nav a { color: var(--muted); }
h1 { font-size: 1.55rem; line-height: 1.25; margin: 0 0 .35rem; }
h2 { font-size: 1.2rem; margin: 2rem 0 .5rem; padding-top: .5rem;
border-top: 1px solid var(--line); }
h3 { font-size: 1.02rem; margin: 1.4rem 0 .4rem; }
p, ul, ol { margin: .6rem 0; }
ul, ol { padding-left: 1.3rem; }
li { margin: .25rem 0; }
li.task { list-style: none; margin-left: -1.3rem; }
li.task input { margin-right: .45rem; }
a { color: var(--accent); }
a.ext::after { content: " ↗"; font-size: .8em; color: var(--muted); }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em;
background: var(--chip); padding: .1em .35em; border-radius: 4px; }
pre { background: var(--chip); padding: .75rem; border-radius: 6px; overflow-x: auto; }
pre code { background: none; padding: 0; }
blockquote { margin: .9rem 0; padding: .6rem .9rem; background: var(--quote);
border-left: 3px solid var(--accent); border-radius: 0 6px 6px 0; }
blockquote p { margin: .2rem 0; }
hr { border: 0; border-top: 1px solid var(--line); margin: 1.5rem 0; }
.table-wrap { overflow-x: auto; margin: .8rem 0; -webkit-overflow-scrolling: touch; }
table { border-collapse: collapse; width: 100%; font-size: .92rem; }
th, td { border: 1px solid var(--line); padding: .4rem .55rem; vertical-align: top; }
th { background: var(--chip); font-weight: 600; }
.chip { display: inline-block; background: var(--chip); color: var(--muted);
font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
padding: .15rem .5rem; border-radius: 999px; }
.lead { color: var(--muted); margin: .35rem 0 1.25rem; }
.meta { margin: 0 0 1.5rem; }
.meta summary { cursor: pointer; font-size: .85rem; color: var(--muted);
padding: .3rem 0; }
.meta table { font-size: .85rem; }
.meta th { width: 38%; text-align: left; font-weight: 500; word-break: break-word; }
.cards { list-style: none; padding: 0; margin: 0; }
.cards li { border: 1px solid var(--line); border-radius: 8px; padding: .8rem .9rem;
margin: .6rem 0; }
.cards a { font-weight: 600; text-decoration: none; }
.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); }
</style>
</head>
<body>
<nav><a href="index.html">← Wszystkie karty</a> · <a href="../viz.html">Graf</a></nav>
<span class="chip">region</span>
<h1>Skicircus Saalbach — informacje o regionie</h1>
<p class="lead">Wszystko poza noclegiem: teren narciarski, skipassy ALPIN CARD, dojazd z Warszawy, analiza terminów, koszty życia i budżet całkowity grupy.</p>
<details class="meta" open><summary>Metadane (frontmatter)</summary><div class="table-wrap"><table><tbody><tr><th>type</th><td>region</td></tr><tr><th>title</th><td>Skicircus Saalbach-Hinterglemm-Leogang-Fieberbrunn</td></tr><tr><th>description</th><td>Wszystko poza noclegiem: teren narciarski, skipassy ALPIN CARD, dojazd z Warszawy, analiza terminów, koszty życia i budżet całkowity grupy.</td></tr><tr><th>timestamp</th><td>2026-07-31T00:00:00Z</td></tr><tr><th>tags</th><td>region, saalbach, skipass, logistyka</td></tr><tr><th>id</th><td>skicircus-saalbach</td></tr><tr><th>nazwa</th><td>Skicircus Saalbach-Hinterglemm-Leogang-Fieberbrunn</td></tr><tr><th>kraj</th><td>Austria</td></tr><tr><th>region</th><td>Salzburg (Pinzgau)</td></tr><tr><th>sezon</th><td>2026/2027</td></tr><tr><th>grupa</th><td>4 rodziny: 2×(2+2 nastolatki) + 2×(2+1 dziecko) = 14 osób</td></tr><tr><th>wyjazd_z</th><td>Warszawa</td></tr><tr><th>rekomendowany_termin</th><td>1623.01.2027 (alternatywa: 916.01.2027)</td></tr><tr><th>ostatnia_aktualizacja</th><td>2026-07-31</td></tr></tbody></table></div></details>
<p>Baza wiedzy wyjazdu narciarskiego zimą 2026/27. Ten dokument zbiera wszystko, co <strong>nie dotyczy konkretnego noclegu</strong>: teren narciarski, skipassy, dojazd, terminy, koszty życia i logistykę grupy. Oferty noclegowe → katalog <a href="index.html"><code>oferty/</code></a>.</p>
<h2>Dlaczego ten region</h2>
<ul><li><strong>270 km połączonych tras</strong> — największy w pełni połączony teren w Salzburgu, klimat "wielkiej karuzeli" najbliższy Sella Rondzie (którą grupa zna i lubi, ale była tam już 2×).</li><li>Objazdowa pętla <strong>"Skicircus challenge"</strong> — całodzienna runda po całym terenie, atrakcyjna dla nastolatków (odpowiednik pętli Sella Ronda).</li><li>Snowparki (Nightpark Hinterglemm, Learn-to-Ride Leogang), junior race'y.</li><li><strong>Najkrótszy dojazd z Warszawy</strong> spośród rozważanych dużych ośrodków (~950 km; dla porównania: Sella Ronda ~1 100 km, Livigno ~1 250 km).</li></ul>
<h2>Skipass — ALPIN CARD</h2>
<p>Karnet <strong>Ski ALPIN CARD</strong> obejmuje: Skicircus Saalbach-Hinterglemm-Leogang- Fieberbrunn + Schmittenhöhe (Zell am See) + lodowiec Kitzsteinhorn (Kaprun). Istotne: gondola zellamseeXpress w Viehhofen też jest na tym karnecie.</p>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">Taryfa 6 dni</th><th style="text-align:left">Cena 2025/26 (orient.)</th><th style="text-align:left">Prognoza 2026/27 (+35%)</th></tr></thead><tbody><tr><td style="text-align:left">Dorosły</td><td style="text-align:left">~430450 €</td><td style="text-align:left">~445470 €</td></tr><tr><td style="text-align:left">Młodzież (do 18 lat)</td><td style="text-align:left">~ -25% od ceny dorosłego</td><td style="text-align:left">~335355 €</td></tr><tr><td style="text-align:left">Dziecko (do ~15 lat)</td><td style="text-align:left">~ -50%</td><td style="text-align:left">~225235 €</td></tr></tbody></table></div>
<p><strong>Szacunkowy koszt skipassów dla grupy 14 os.</strong> (8 dorosłych + 4 nastolatków</p>
<ul><li>2 dzieci, 6 dni): <strong>~4 9005 400 €</strong> łącznie, czyli:</li></ul>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">Rodzina</th><th style="text-align:left">Skład</th><th style="text-align:left">Skipassy 6 dni</th></tr></thead><tbody><tr><td style="text-align:left">2+2 (nastolatki)</td><td style="text-align:left">2 dorosłych + 2 młodzież</td><td style="text-align:left">~1 5701 650 €</td></tr><tr><td style="text-align:left">2+1 (dziecko)</td><td style="text-align:left">2 dorosłych + 1 dziecko</td><td style="text-align:left">~1 1201 175 €</td></tr></tbody></table></div>
<blockquote><p>⚠️ Ceny 26/27 do potwierdzenia jesienią 2026 na alpincard.at. Dzieci/młodzież: decyduje rocznik, nie wiek w dniu wyjazdu — sprawdzić dokładne progi rocznikowe przy zakupie.</p></blockquote>
<h2>Dojazd z Warszawy</h2>
<ul><li><strong>Trasa:</strong> Warszawa → Katowice → Ostrawa → Brno → Wiedeń → Salzburg → Zell am See → dolina Glemmtal. <strong>~950 km, 910 h jazdy</strong> (realnie 1112 h z przerwami). Da się w jeden dzień.</li><li><strong>Opłaty:</strong> winiety Czechy (~14 €/10 dni) + Austria (~13 €/10 dni, 2026); odcinek specjalny A10 (tunele Tauern) ~14 € w jedną stronę jeśli trasa przez SalzburgVillach (uwaga: do Zell am See zwykle zjazd wcześniej — bez opłaty specjalnej).</li><li><strong>Paliwo:</strong> ~1 900 km w obie strony → ~150170 l → <strong>~9501 100 zł/auto</strong> (diesel, spalanie 8 l/100 km).</li><li><strong>Łączny koszt dojazdu:</strong> ~300350 €/auto w obie strony. Grupa jedzie 4 autami (po 1 na rodzinę) lub 3 (oszczędność ~300 €, ale mniej elastycznie).</li><li>Łańcuchy/opony zimowe obowiązkowe; ostatnie 15 km doliną Glemmtal — droga lokalna, zimą odśnieżana na bieżąco.</li></ul>
<h2>Terminy — analiza okna 123.01.2027</h2>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">Tydzień</th><th style="text-align:left">Ocena</th><th style="text-align:left">Uzasadnienie</th></tr></thead><tbody><tr><td style="text-align:left">29.01</td><td style="text-align:left">⚠️ średnio</td><td style="text-align:left">ogon szczytu noworocznego: tłumy do ~6.01, najwyższe ceny noclegów</td></tr><tr><td style="text-align:left">916.01</td><td style="text-align:left">✅ dobry</td><td style="text-align:left">pierwszy "pusty" tydzień zimy, niższe ceny, świetny śnieg</td></tr><tr><td style="text-align:left"><strong>1623.01</strong></td><td style="text-align:left">✅✅ <strong>rekomendowany</strong></td><td style="text-align:left">najluźniejsze stoki, pełna dostępność noclegów (m.in. jedyne okno na 2× Glemm Lodge), te same ceny co 916.01</td></tr></tbody></table></div>
<h2>Koszty życia na miejscu</h2>
<ul><li>Obiad na stoku: 1522 €/os. (spaghetti/schnitzel + napój).</li><li>Zakupy spożywcze: MPREIS/Billa w Saalbach i Hinterglemm; ceny ~2030% wyższe niż w PL. Patent grupowy: duże zakupy w Zell am See (Lidl, Spar) po drodze.</li><li>Wieczorne wyjście na pizzę/burgery: 1825 €/os.</li><li><strong>Taksa klimatyczna:</strong> ~23 €/os./noc (dorośli; wg gminy), płatna na miejscu.</li><li>Skibus w dolinie: <strong>bezpłatny ze skipassem</strong>, kursuje Saalbach ↔ Hinterglemm ↔ Viehhofen co 1020 min.</li></ul>
<h2>Budżet całkowity grupy (orientacyjnie, tydzień, 6 dni jazdy)</h2>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">Pozycja</th><th style="text-align:left">14 osób łącznie</th></tr></thead><tbody><tr><td style="text-align:left">Noclegi (cel: ~1 2002 000 €/apartament)</td><td style="text-align:left">5 5007 500 €</td></tr><tr><td style="text-align:left">Skipassy</td><td style="text-align:left">4 9005 400 €</td></tr><tr><td style="text-align:left">Dojazd (4 auta)</td><td style="text-align:left">1 2001 400 €</td></tr><tr><td style="text-align:left">Jedzenie + taksy</td><td style="text-align:left">2 5003 500 €</td></tr><tr><td style="text-align:left"><strong>RAZEM</strong></td><td style="text-align:left"><strong>~14 00018 000 €</strong> (≈ 1 0001 300 €/os.)</td></tr></tbody></table></div>
<h2>Kalendarz decyzji</h2>
<ol><li><strong>Sierpień 2026</strong> — wysłanie zapytań grupowych do obiektów (<a href="szablon-maila.html">szablon maila</a>), zebranie ofert, wybór.</li><li><strong>Wrzesień 2026</strong> — rezerwacja + zaliczki (typowo 30%).</li><li><strong>~16.10.2026</strong> — ostateczny deadline darmowej anulacji dla Glemm Lodge (91 dni przed 16.01); inne obiekty: sprawdzić indywidualnie.</li><li><strong>Jesień 2026</strong> — potwierdzenie cen skipassów 26/27, decyzja o zakupie online (ewentualne przedsprzedaże).</li><li><strong>Grudzień 2026</strong> — dopłaty (typowo 90 dni przed), organizacja aut, sprzętu, ubezpieczeń (rekomendowane: koszty rezygnacji + NNW/OC narciarskie).</li></ol>
<footer>Wygenerowane z bundle'a OKF przez <code>gen_pages.py</code> — nie edytuj
tych plików, edytuj <code>.md</code> i przegeneruj.</footer>
</body>
</html>

86
pages/szablon-maila.html Normal file
View file

@ -0,0 +1,86 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Szablon zapytania grupowego (EN) — Ski 2027 — Skicircus Saalbach</title>
<style>
:root { color-scheme: light dark; --fg:#0f172a; --muted:#64748b; --bg:#ffffff;
--line:#e2e8f0; --accent:#0d9488; --chip:#f1f5f9; --quote:#f8fafc; }
@media (prefers-color-scheme: dark) {
:root { --fg:#e2e8f0; --muted:#94a3b8; --bg:#0f172a; --line:#334155;
--accent:#2dd4bf; --chip:#1e293b; --quote:#1e293b; }
}
* { box-sizing: border-box; }
body { margin: 0 auto; padding: 1.25rem 1rem 4rem; max-width: 46rem;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 17px; line-height: 1.55; color: var(--fg); background: var(--bg);
-webkit-text-size-adjust: 100%; }
nav { font-size: .85rem; margin-bottom: 1.25rem; }
nav a { color: var(--muted); }
h1 { font-size: 1.55rem; line-height: 1.25; margin: 0 0 .35rem; }
h2 { font-size: 1.2rem; margin: 2rem 0 .5rem; padding-top: .5rem;
border-top: 1px solid var(--line); }
h3 { font-size: 1.02rem; margin: 1.4rem 0 .4rem; }
p, ul, ol { margin: .6rem 0; }
ul, ol { padding-left: 1.3rem; }
li { margin: .25rem 0; }
li.task { list-style: none; margin-left: -1.3rem; }
li.task input { margin-right: .45rem; }
a { color: var(--accent); }
a.ext::after { content: " ↗"; font-size: .8em; color: var(--muted); }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em;
background: var(--chip); padding: .1em .35em; border-radius: 4px; }
pre { background: var(--chip); padding: .75rem; border-radius: 6px; overflow-x: auto; }
pre code { background: none; padding: 0; }
blockquote { margin: .9rem 0; padding: .6rem .9rem; background: var(--quote);
border-left: 3px solid var(--accent); border-radius: 0 6px 6px 0; }
blockquote p { margin: .2rem 0; }
hr { border: 0; border-top: 1px solid var(--line); margin: 1.5rem 0; }
.table-wrap { overflow-x: auto; margin: .8rem 0; -webkit-overflow-scrolling: touch; }
table { border-collapse: collapse; width: 100%; font-size: .92rem; }
th, td { border: 1px solid var(--line); padding: .4rem .55rem; vertical-align: top; }
th { background: var(--chip); font-weight: 600; }
.chip { display: inline-block; background: var(--chip); color: var(--muted);
font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
padding: .15rem .5rem; border-radius: 999px; }
.lead { color: var(--muted); margin: .35rem 0 1.25rem; }
.meta { margin: 0 0 1.5rem; }
.meta summary { cursor: pointer; font-size: .85rem; color: var(--muted);
padding: .3rem 0; }
.meta table { font-size: .85rem; }
.meta th { width: 38%; text-align: left; font-weight: 500; word-break: break-word; }
.cards { list-style: none; padding: 0; margin: 0; }
.cards li { border: 1px solid var(--line); border-radius: 8px; padding: .8rem .9rem;
margin: .6rem 0; }
.cards a { font-weight: 600; text-decoration: none; }
.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); }
</style>
</head>
<body>
<nav><a href="index.html">← Wszystkie karty</a> · <a href="../viz.html">Graf</a></nav>
<span class="chip">narzedzie</span>
<h1>Szablon maila — zapytanie grupowe (EN)</h1>
<p class="lead">Gotowy angielski mail z zapytaniem o 4 apartamenty dla 14 osób w terminie 1623.01.2027 wraz z notatkami negocjacyjnymi.</p>
<details class="meta" open><summary>Metadane (frontmatter)</summary><div class="table-wrap"><table><tbody><tr><th>type</th><td>narzedzie</td></tr><tr><th>title</th><td>Szablon zapytania grupowego (EN)</td></tr><tr><th>description</th><td>Gotowy angielski mail z zapytaniem o 4 apartamenty dla 14 osób w terminie 1623.01.2027 wraz z notatkami negocjacyjnymi.</td></tr><tr><th>timestamp</th><td>2026-07-31T00:00:00Z</td></tr><tr><th>tags</th><td>szablon, komunikacja, rezerwacja</td></tr><tr><th>id</th><td>szablon-maila</td></tr><tr><th>nazwa</th><td>Szablon zapytania grupowego (EN)</td></tr><tr><th>ostatnia_aktualizacja</th><td>2026-07-31</td></tr></tbody></table></div></details>
<p>Wysłać ten sam mail do 23 obiektów równolegle, podmieniając [pola]. Wersja uniwersalna pod 4 apartamenty; przy Sportchalet/Glemm Lodge dopisać pytanie o zajęcie całego obiektu.</p>
<hr>
<p><strong>Subject:</strong> Group inquiry — 4 apartments, week 1623 Jan 2027 (14 persons)</p>
<p>Dear [Sir or Madam / imię gospodarza],</p>
<p>We are a group of four families from Poland (14 persons in total: 8 adults, 4 teenagers and 2 children) planning a ski week in Saalbach in January 2027.</p>
<p>We would like to ask about availability and a group offer for:</p>
<ul><li><strong>4 apartments</strong> (2× for 4 persons: 2 adults + 2 teenagers; 2× for 3 persons: 2 adults + 1 child)</li><li><strong>Arrival Sat 16 January departure Sat 23 January 2027</strong> (7 nights)</li><li>Alternative dates: 916 January 2027</li></ul>
<p>Could you please send us:</p>
<ol><li>Availability for these dates and apartment types,</li><li>Your best total price for the group (incl. final cleaning and taxes),</li><li>Deposit and cancellation terms.</li></ol>
<p>Since we are booking four apartments for a full week directly with you, we would appreciate a group rate if possible.</p>
<p>Best regards, [Imię i nazwisko] [Telefon / e-mail]</p>
<hr>
<h2>Notatki taktyczne</h2>
<ul><li>Wysyłać z jednego "kontaktowego" adresu — jedna osoba negocjuje za grupę.</li><li>Nie podawać budżetu w pierwszym mailu; najpierw ich oferta.</li><li>Po otrzymaniu 2 ofert można uczciwie napisać "we received a comparable offer at X €" — w Alpach to normalna praktyka.</li><li>Prosić o ofertę mailem (pisemnie), nie telefonicznie.</li></ul>
<footer>Wygenerowane z bundle'a OKF przez <code>gen_pages.py</code> — nie edytuj
tych plików, edytuj <code>.md</code> i przegeneruj.</footer>
</body>
</html>

81
pages/wyjazd.html Normal file
View file

@ -0,0 +1,81 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ski 2027 — Skicircus Saalbach — Ski 2027 — Skicircus Saalbach</title>
<style>
:root { color-scheme: light dark; --fg:#0f172a; --muted:#64748b; --bg:#ffffff;
--line:#e2e8f0; --accent:#0d9488; --chip:#f1f5f9; --quote:#f8fafc; }
@media (prefers-color-scheme: dark) {
:root { --fg:#e2e8f0; --muted:#94a3b8; --bg:#0f172a; --line:#334155;
--accent:#2dd4bf; --chip:#1e293b; --quote:#1e293b; }
}
* { box-sizing: border-box; }
body { margin: 0 auto; padding: 1.25rem 1rem 4rem; max-width: 46rem;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 17px; line-height: 1.55; color: var(--fg); background: var(--bg);
-webkit-text-size-adjust: 100%; }
nav { font-size: .85rem; margin-bottom: 1.25rem; }
nav a { color: var(--muted); }
h1 { font-size: 1.55rem; line-height: 1.25; margin: 0 0 .35rem; }
h2 { font-size: 1.2rem; margin: 2rem 0 .5rem; padding-top: .5rem;
border-top: 1px solid var(--line); }
h3 { font-size: 1.02rem; margin: 1.4rem 0 .4rem; }
p, ul, ol { margin: .6rem 0; }
ul, ol { padding-left: 1.3rem; }
li { margin: .25rem 0; }
li.task { list-style: none; margin-left: -1.3rem; }
li.task input { margin-right: .45rem; }
a { color: var(--accent); }
a.ext::after { content: " ↗"; font-size: .8em; color: var(--muted); }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em;
background: var(--chip); padding: .1em .35em; border-radius: 4px; }
pre { background: var(--chip); padding: .75rem; border-radius: 6px; overflow-x: auto; }
pre code { background: none; padding: 0; }
blockquote { margin: .9rem 0; padding: .6rem .9rem; background: var(--quote);
border-left: 3px solid var(--accent); border-radius: 0 6px 6px 0; }
blockquote p { margin: .2rem 0; }
hr { border: 0; border-top: 1px solid var(--line); margin: 1.5rem 0; }
.table-wrap { overflow-x: auto; margin: .8rem 0; -webkit-overflow-scrolling: touch; }
table { border-collapse: collapse; width: 100%; font-size: .92rem; }
th, td { border: 1px solid var(--line); padding: .4rem .55rem; vertical-align: top; }
th { background: var(--chip); font-weight: 600; }
.chip { display: inline-block; background: var(--chip); color: var(--muted);
font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
padding: .15rem .5rem; border-radius: 999px; }
.lead { color: var(--muted); margin: .35rem 0 1.25rem; }
.meta { margin: 0 0 1.5rem; }
.meta summary { cursor: pointer; font-size: .85rem; color: var(--muted);
padding: .3rem 0; }
.meta table { font-size: .85rem; }
.meta th { width: 38%; text-align: left; font-weight: 500; word-break: break-word; }
.cards { list-style: none; padding: 0; margin: 0; }
.cards li { border: 1px solid var(--line); border-radius: 8px; padding: .8rem .9rem;
margin: .6rem 0; }
.cards a { font-weight: 600; text-decoration: none; }
.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); }
</style>
</head>
<body>
<nav><a href="index.html">← Wszystkie karty</a> · <a href="../viz.html">Graf</a></nav>
<span class="chip">Trip</span>
<h1>Ski 2027 — Skicircus Saalbach</h1>
<p class="lead">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ę.</p>
<details class="meta" open><summary>Metadane (frontmatter)</summary><div class="table-wrap"><table><tbody><tr><th>type</th><td>Trip</td></tr><tr><th>title</th><td>Ski 2027 — Skicircus Saalbach</td></tr><tr><th>description</th><td>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ę.</td></tr><tr><th>timestamp</th><td>2026-07-31T00:00:00Z</td></tr><tr><th>tags</th><td>wyjazd, saalbach, 2027, planowanie</td></tr><tr><th>nazwa</th><td>Ski 2027 — Skicircus Saalbach: baza wiedzy wyjazdu</td></tr><tr><th>grupa</th><td>4 rodziny (2×2+2, 2×2+1) = 14 osób, wyjazd z Warszawy</td></tr><tr><th>termin</th><td>1623.01.2027 (alt. 916.01)</td></tr><tr><th>ostatnia_aktualizacja</th><td>2026-07-31</td></tr></tbody></table></div></details>
<p>Wyjazd narciarski 4 rodzin: 2×(2+2 nastolatki) + 2×(2+1 dziecko) = <strong>14 osób</strong>, wyjazd samochodami z Warszawy.</p>
<ul><li><strong>Termin:</strong> 1623.01.2027 (alternatywa: 916.01.2027).</li><li><strong>Cel budżetowy noclegu:</strong> <strong>~2 000 €/rodzinę za tydzień</strong> (nie 34 tys.).</li></ul>
<p>Kontekst regionu, skipassów, dojazdu i budżetu całkowitego → <a href="region.html">region</a>. Karty noclegów → <a href="index.html">oferty</a>.</p>
<h2>Oferty — ranking</h2>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">#</th><th style="text-align:left">Oferta</th><th style="text-align:left">Miejscowość</th><th style="text-align:left">€/rodzinę 2+2</th><th style="text-align:left">Status</th><th style="text-align:left">Priorytet</th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left"><a href="oferty/kristall-schattbergxpress.html">Kristall @ SchattbergXpress</a></td><td style="text-align:left">Saalbach centrum, 50 m od gondoli</td><td style="text-align:left">~1 5002 000</td><td style="text-align:left">szacunek</td><td style="text-align:left"><strong>PLAN A</strong></td></tr><tr><td style="text-align:left">2</td><td style="text-align:left"><a href="oferty/alpenparks-sonnleiten.html">AlpenParks Sonnleiten</a></td><td style="text-align:left">Saalbach, 2 min od stoku</td><td style="text-align:left">~1 4001 900</td><td style="text-align:left">szacunek</td><td style="text-align:left">PLAN A-bis</td></tr><tr><td style="text-align:left">3</td><td style="text-align:left"><a href="oferty/sportchalet-viehhofen.html">Sportchalet Viehhofen</a></td><td style="text-align:left">Viehhofen, skibus</td><td style="text-align:left">~1 2001 600</td><td style="text-align:left">szacunek</td><td style="text-align:left"><strong>PLAN B</strong></td></tr><tr><td style="text-align:left">4</td><td style="text-align:left"><a href="oferty/dom-dla-grupy.html">Dom 1214 os.</a></td><td style="text-align:left">Viehhofen/Maishofen/Fieberbrunn</td><td style="text-align:left">~1 4001 900</td><td style="text-align:left">koncept</td><td style="text-align:left">równoległy</td></tr><tr><td style="text-align:left">5</td><td style="text-align:left"><a href="oferty/alpenparks-rehrenberg.html">AlpenParks Rehrenberg</a></td><td style="text-align:left">Viehhofen</td><td style="text-align:left">~1 1001 500</td><td style="text-align:left">szacunek</td><td style="text-align:left">PLAN C</td></tr><tr><td style="text-align:left">6</td><td style="text-align:left"><a href="oferty/glemm-lodge.html">Glemm Lodge</a></td><td style="text-align:left">Saalbach/Hinterglemm</td><td style="text-align:left">~4 370</td><td style="text-align:left"><strong>zweryfikowane</strong></td><td style="text-align:left">odrzucona (referencja)</td></tr></tbody></table></div>
<h2>Najbliższe kroki</h2>
<ol><li>Mail grupowy (<a href="szablon-maila.html">szablon</a>) → Kristall/All in One + Sportchalet Viehhofen.</li><li>Ceny online datami 1623.01.2027 → AlpenParks (oba obiekty).</li><li>Zebranie ofert → decyzja grupy → zaliczki (wrzesień 2026).</li></ol>
<h2>Konwencja danych</h2>
<p>Każda karta oferty ma frontmatter YAML z polami OKF (<code>type</code>, <code>title</code>, <code>description</code>, <code>timestamp</code>, <code>tags</code>) oraz z polami rozszerzeń: <code>id</code>, <code>nazwa</code>, <code>miejscowosc</code>, <code>gps</code>, <code>odleglosc_od_wyciagu</code>, <code>ocena</code>, <code>cena_za_apartament_tydzien</code>, <code>koszt_rodzina_2p2</code>, <code>koszt_rodzina_2p1</code>, <code>status_ceny</code> (SZACUNEK / ZWERYFIKOWANE), <code>priorytet</code>, <code>www</code>, <code>ostatnia_aktualizacja</code> — stronę można generować statycznie prosto z tych pól (Astro/Hugo/Eleventy czytają frontmatter natywnie).</p>
<footer>Wygenerowane z bundle'a OKF przez <code>gen_pages.py</code> — nie edytuj
tych plików, edytuj <code>.md</code> i przegeneruj.</footer>
</body>
</html>