website: M3 part A - the controller's shell, and the home page inside it
gates / gates (push) Successful in 6m58s

The direction the operator chose: the controller's FRAME around the website's own
content. Staged at /preview/m3/, noindex, nothing live.

website/assets/shell.css - 117 rules COPIED VERBATIM from felhom-controller
  style.css @ dcd89364a4 (sidebar, mobile top bar, off-canvas drawer, and the
  component atoms the restyled pages use: btn, alert, app-info-card, launch-*,
  stack-detail-card, form-*). Exactly ONE rule is not verbatim and says so:
  body.shell { display:flex; min-height:100vh }, which lives in the controller's
  base rather than in a component, scoped to .shell so other pages are untouched.

  The generator NAMES every rule it copies and REFUSES to write the file if one has
  disappeared upstream. That is not ceremony: it already caught two things this
  session. It refused because .sidebar::-webkit-scrollbar does not exist (only the
  -track override does), and because prefers-reduced-motion is split across FIVE
  separate blocks in the controller - reading only the first would have silently
  lost four rules. The M1 round lost a rule exactly this way.

  The bare `body { flex-direction: column }` that broke M1's phone layout is copied
  this time, by name, as are body.nav-open and the no-js/js pair.

  Load order is deliberate: shell.css AFTER site.css. Thirteen selectors exist in
  both files with different meanings (.nav-links, .page-header, .lang-globe*, .ico)
  - a horizontal site nav there, a vertical sidebar here. Later wins, and a page
  that does not load shell.css is completely unaffected.

website/preview/m3/index.html - the home page in the shell. Content is /preview/'s
  cleaned text; body carries `shell page-index` so the existing .page-index content
  rules (caps, flow, vs, band, who-mini) are reused rather than duplicated. Six
  sections, left-aligned headers, the controller's cards. Head tags (title, meta
  description, canonical, hreflang) are already the FINAL values, so the switch in
  part D needs no head edit.

  Four changes from the operator's mockup, each with its reason in the generator:
    1. the active item follows the page (the mockups show Inditopult active on all)
    2. Teszteles is ALSO a primary button at the top of the lower group - it is the
       one action we want and the mockup buries it at the bottom
    3. the Alkalmazasok categories are anchors on /alkalmazasok: real URLs, no
       JS-only state
    4. ADDED GYIK. It is in today's nav; dropping it silently would take a page out
       of navigation, which is not a change a restyle gets to make.

scripts/site_gates.py - gate 3 extended rather than duplicated, as asked: a SHELL
  set whose pages must carry an IDENTICAL <nav class="sidebar"> after the active
  marker is normalised. RED-PROOFED: a one-character change to one menu label on a
  second shell page convicts. It also prints "DIFFER" rather than a reassuring
  "identical" count when it fails - a false reassurance printed beside a conviction
  is the exact defect this repo files rows about.

documentation/design/design-system.md - new "Shell" section: what is shared (tokens
  + shell + atoms), what each side owns, how a change flows (controller first, the
  website's copy the same week), and that the cross-repo gate is deliberately NOT
  built yet.
This commit is contained in:
2026-10-11 13:48:50 +02:00
parent d5f00b7115
commit 80659232bc
5 changed files with 1209 additions and 2 deletions
+53
View File
@@ -160,6 +160,59 @@ scale addition: hero H1 up to ~3.5rem/800. Assets are cache-busted with `?v=N` (
nav/footer consistency, analytics presence, no-CDN, banned tokens, zero `<style>` blocks, nav/footer consistency, analytics presence, no-CDN, banned tokens, zero `<style>` blocks,
cache-busting). cache-busting).
## Shell (M3, 2026-10-11)
**The website and the controller now share two things: the tokens, and the shell.** Everything else
each owns.
| | Shared | Website owns | Controller owns |
|---|---|---|---|
| Tokens | **yes** — 18 names, verified byte-identical in both `:root` blocks | — | — |
| Shell: sidebar, mobile top bar, off-canvas drawer | **yes** — `website/assets/shell.css` is a COPY | the menu's items, the wordmark under the logo, the sidebar's tester button, the language globe's targets | the menu's items, the language globe's POST form |
| Component atoms: `.btn*`, `.alert*`, `.app-info-card`, `.launch-*`, `.stack-detail-card`, `.form-*` | **yes** — same copy | — | — |
| Page content and its layout | no | all of it (`site.css`) | all of it (`style.css`) |
**`website/assets/shell.css` is a copy, not a library.** It is generated from
`felhom-controller/controller/internal/web/templates/style.css` and carries that commit in its
header. Exactly one rule in it is not verbatim: `body.shell { display: flex; min-height: 100vh }`,
restated because it lives in the controller's base rather than in a component, and scoped to
`.shell` so a page without the shell is untouched.
**Load order matters and is deliberate.** `shell.css` loads AFTER `site.css`. Thirteen selectors
exist in both with different meanings — `.nav-links`, `.nav-links a`, `.page-header`, `.lang-globe*`,
`.ico`, `.ico-sm` — a horizontal site nav in one, a vertical sidebar in the other. Later wins, so a
shell page gets the shell's meaning and a page that does not load `shell.css` is unaffected.
### How a change flows
1. **The controller changes first.** It is the surface a household operates daily; the website
follows it, never the other way round.
2. **The website's copy is re-generated in the same week**, by re-running the generator against the
controller's new commit and updating the header. A copy that silently falls behind is the failure
this section exists to prevent.
3. **A later gate may enforce step 2. One is NOT built today** (operator's brief, 2026-10-11) — the
cross-repo check waits until the shell has settled.
### The trap that makes this a copy with a generator, rather than a copy by hand
Copying components by class name **silently drops the element-level rules those components depend
on.** Measured on 2026-10-11: copying the whole `@media(max-width: 768px)` block *except* its bare
`body { flex-direction: column }` left the sticky top bar as a row flex item, pushed the content
135 px right, collapsed the launcher grid to one column, made a page 15 597 px tall on a phone and
overflowed two others. Everything looked copied. Full account:
`documentation/audits/mockups-2026-10-11/SHARED-DESIGN.md` §2.
So the generator **names every rule it copies and refuses to write the file if one has disappeared
upstream** — an incomplete copy fails loudly instead of rendering wrong.
### Enforcement for the shell
`scripts/site_gates.py` gate 3b: every page in the `SHELL` set must carry a
`<nav class="sidebar">` block **identical** to the others' after the active marker is normalised
away. The site is static with no build step, so the shell is duplicated into each page; this is what
keeps the duplication honest. Red-proofed 2026-10-11: a one-character difference in one menu label
convicts.
## Enforcement ## Enforcement
The D0 grep gate (all counts must be 0 in `controller/internal/{web,setup}`): The D0 grep gate (all counts must be 0 in `controller/internal/{web,setup}`):
+40 -2
View File
@@ -115,9 +115,16 @@ MOCKUPS = {os.path.join("preview", "m1", "index.html"),
os.path.join("preview", "m1", "kapcsolat.html"), os.path.join("preview", "m1", "kapcsolat.html"),
os.path.join("preview", "m2", "index.html")} os.path.join("preview", "m2", "index.html")}
PAGES = list(TWINS) + list(TWINS.values()) + sorted(NO_TWIN) + sorted(MOCKUPS) # SHELL (2026-10-11, M3) — pages that wear the controller's frame instead of the website's
# <nav>/<footer>. They are staged under /preview/m3/ until the operator's go, and at the switch
# every live page becomes one of these. They keep every other check; what changes for them is
# WHICH block has to be identical across the set: the sidebar, not the nav and footer. Gate 3
# below does both, so there is one identity gate rather than two.
SHELL = {os.path.join("preview", "m3", "index.html")}
PAGES = list(TWINS) + list(TWINS.values()) + sorted(NO_TWIN) + sorted(MOCKUPS) + sorted(SHELL)
EN_PAGES = set(TWINS.values()) EN_PAGES = set(TWINS.values())
HU_PAGES = set(PAGES) - EN_PAGES - MOCKUPS HU_PAGES = set(PAGES) - EN_PAGES - MOCKUPS - SHELL
ANALYTICS_EXEMPT = {"szolgaltatasok-nonpublic.html"} | MOCKUPS ANALYTICS_EXEMPT = {"szolgaltatasok-nonpublic.html"} | MOCKUPS
ANALYTICS_MARK = "https://stats.felhom.eu/script.js" ANALYTICS_MARK = "https://stats.felhom.eu/script.js"
SITE = "https://felhom.eu" SITE = "https://felhom.eu"
@@ -225,6 +232,37 @@ for lang, members, ref in (("hu", HU_PAGES, "index.html"), ("en", EN_PAGES, os.p
fail("%s: <nav> differs from %s (after active-marker normalization)" % (p, ref)) fail("%s: <nav> differs from %s (after active-marker normalization)" % (p, ref))
if foot != rfoot: if foot != rfoot:
fail("%s: <footer> differs from %s" % (p, ref)) fail("%s: <footer> differs from %s" % (p, ref))
# gate 3b (2026-10-11, M3): the SHELL set carries a sidebar instead of the nav+footer, and that
# sidebar is duplicated into every page because the site is static with no build step. The thing
# that keeps the duplication honest is this check: every shell page's <nav class="sidebar"> block
# must be identical once the active marker and the globe's current item are normalised away —
# exactly the normalisation norm_block already does for the old chrome.
_shell_by_lang = {}
for p in sorted(SHELL):
s = pages.get(p)
if s is None:
continue
m = re.search(r'<nav class="sidebar" id="sidebar">.*?</nav>', s, re.DOTALL)
if not m:
fail("%s: no <nav class=\"sidebar\"> block — a shell page carries the shell" % p)
continue
_shell_by_lang.setdefault("en" if p.startswith("en" + os.sep) else "hu", []).append((p, norm_block(m.group(0))))
for _lang, _members in sorted(_shell_by_lang.items()):
_ref_p, _ref = _members[0]
_bad = 0
for _p, _blk in _members[1:]:
if _blk != _ref:
_bad += 1
fail("%s: <nav class=\"sidebar\"> differs from %s (after active-marker normalization) — "
"the shell is duplicated into every page, so it must be identical" % (_p, _ref_p))
# say "identical" ONLY when they are. A reassuring count printed next to a conviction is the
# same defect this repo files rows about.
if _bad:
print(" shell sidebar: %d of %d %s page(s) DIFFER from %s"
% (_bad, len(_members) - 1, _lang, _ref_p))
else:
print(" shell sidebar: %d %s page(s), all identical to %s" % (len(_members), _lang, _ref_p))
if os.path.join("en", "index.html") in pages: if os.path.join("en", "index.html") in pages:
_f = re.search(r"<footer>.*?</footer>", pages[os.path.join("en", "index.html")], re.DOTALL) _f = re.search(r"<footer>.*?</footer>", pages[os.path.join("en", "index.html")], re.DOTALL)
if not _f or "households in Hungary" not in _f.group(0): if not _f or "households in Hungary" not in _f.group(0):
+740
View File
@@ -0,0 +1,740 @@
/* =============================================================================
shell.css - the controller's frame, around the website's own content.
COPIED, NOT WRITTEN. Source: felhom-controller
controller/internal/web/templates/style.css @ dcd89364a4
(the markup it styles is that repo's templates/layout.html).
KEEP IN STEP WITH THE CONTROLLER - see documentation/design/design-system.md
"Shell". A change flows controller first, then this copy.
Loaded AFTER site.css on purpose. Thirteen selectors exist in both files
(.nav-links, .nav-links a, .page-header, .lang-globe*, .ico, .ico-sm) with
different meanings - a horizontal site nav there, a vertical sidebar here.
Later wins, so the shell's meaning is the one that applies on a shell page.
A page that does not load this file is completely unaffected.
Rules copied VERBATIM; only this header is new. The generator refuses to
write the file if any rule named below has disappeared upstream, so an
incomplete copy fails loudly instead of rendering wrong (the lesson in
documentation/audits/mockups-2026-10-11/SHARED-DESIGN.md section 2).
============================================================================= */
/* ---------- ADAPTED (the only non-verbatim rules in this file) ----------
The controller's BASE gives body `display:flex; min-height:100vh` - that is what puts
the sidebar beside the content. site.css has no such rule, so it is restated here,
SCOPED to .shell so a page without the shell is untouched. For the same reason the two
bare `body` rules in the phone block below are emitted as `body.shell`.
Everything else in this file is verbatim. */
body.shell { display: flex; min-height: 100vh; }
/* ---------- shell, desktop ---------- */
.ico {
width: 16px;
height: 16px;
flex-shrink: 0;
vertical-align: -0.18em;
}
.ico-sm {
width: 14px; height: 14px;
}
.sidebar {
width: 240px;
background: var(--bg-2);
color: var(--text-1);
display: flex;
flex-direction: column;
position: fixed;
height: 100vh;
overflow-y: auto;
border-right: 1px solid var(--line);
}
.sidebar::-webkit-scrollbar-track {
background: var(--bg-2);
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid var(--line);
}
.sidebar-logo {
width: 140px;
height: auto;
display: block;
margin: 0 auto 0.25rem;
}
.sidebar-bottom {
margin-top: auto;
border-top: 1px solid var(--line);
}
.sidebar-footer {
padding: 1rem 1.5rem;
border-top: 1px solid var(--line);
display: flex;
justify-content: space-between;
align-items: center;
font-size: .8rem;
}
.nav-links {
list-style: none;
padding: 1rem 0;
flex: 1;
}
.nav-links a {
display: flex;
align-items: center;
gap: .6rem;
padding: .75rem 1.5rem;
color: var(--text-2);
text-decoration: none;
font-size: .95rem;
font-weight: 500;
transition: color 0.2s ease, background 0.2s ease;
}
.nav-links a:hover {
color: var(--blue-bright);
background: var(--blue-dim);
}
.nav-links a.active {
color: var(--blue-bright);
background: var(--blue-dim);
border-left: 3px solid var(--blue);
}
.nav-links-sub {
padding: 0 0 .5rem;
flex: none;
}
.nav-links-sub a {
padding: .5rem 1.5rem .5rem 1.75rem;
font-size: .88rem;
}
.nav-links-nested {
padding: 0;
list-style: none;
}
.nav-links-nested a {
padding: .4rem 1.5rem .4rem 3.1rem;
font-size: .85rem;
}
.nav-links-nested a.active {
border-left: 3px solid var(--blue);
}
.nav-group-label {
padding: .75rem 1.5rem .25rem;
font-size: .68rem;
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase;
color: var(--text-3);
}
.nav-group {
display: grid;
grid-template-rows: auto 0fr;
transition: grid-template-rows .18s ease;
}
.nav-group.is-open {
grid-template-rows: auto 1fr;
}
.nav-group > .nav-links-nested {
overflow: hidden;
min-height: 0;
}
.nav-group-toggle {
display: flex;
align-items: center;
gap: .6rem;
width: 100%;
padding: .75rem 1.5rem;
background: none;
border: none;
border-left: 3px solid transparent;
color: var(--text-2);
font-family: inherit;
font-size: .95rem;
font-weight: 500;
text-align: left;
cursor: pointer;
transition: color 0.2s ease, background 0.2s ease;
}
.nav-group-toggle:hover {
color: var(--blue-bright);
background: var(--blue-dim);
}
.nav-group-toggle.active {
color: var(--blue-bright);
background: var(--blue-dim);
border-left-color: var(--blue);
}
.nav-group-toggle:focus-visible {
outline: 2px solid var(--blue-bright);
outline-offset: -2px;
}
.nav-chevron {
margin-left: auto;
width: 14px;
height: 14px;
color: var(--text-3);
transition: transform .18s ease;
}
.nav-group.is-open .nav-chevron {
transform: rotate(180deg);
}
.mobile-topbar {
display: none;
}
.nav-backdrop {
display: none;
}
.nav-burger {
display: flex;
align-items: center;
padding: .375rem;
background: none;
border: none;
color: var(--text-2);
cursor: pointer;
transition: color 0.2s ease;
}
.nav-burger:hover {
color: var(--blue-bright);
}
.nav-burger:focus-visible {
outline: 2px solid var(--blue-bright);
outline-offset: -2px;
}
.nav-burger .ico {
width: 24px; height: 24px;
}
.content {
margin-left: 240px;
padding: 2rem;
flex: 1;
max-width: 1200px;
}
.version {
color: var(--text-3);
}
.logout-link {
color: var(--text-3); text-decoration: none; transition: color 0.2s ease;
}
.logout-link:hover {
color: var(--blue-bright);
}
.lang-globe {
position: relative; margin: 0;
}
.lang-globe-btn {
list-style: none; display: inline-flex; align-items: center; color: var(--text-3); cursor: pointer; padding: .15rem; border-radius: var(--radius);
}
.lang-globe-btn::-webkit-details-marker {
display: none;
}
.lang-globe-btn:hover, .lang-globe[open] .lang-globe-btn {
color: var(--blue-bright);
}
.lang-globe-menu {
position: absolute; bottom: calc(100% + .35rem); left: 50%; transform: translateX(-50%); margin: 0; padding: .25rem; list-style: none; min-width: 7.5rem; background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius); z-index: 20;
}
.lang-globe-menu li {
margin: 0;
}
.lang-globe-item {
display: block; width: 100%; text-align: left; background: none; border: none; color: var(--text-2); font: inherit; font-size: .8rem; padding: .3rem .5rem; border-radius: var(--radius); cursor: pointer; white-space: nowrap;
}
.lang-globe-item:hover {
color: var(--blue-bright);
}
.lang-globe-item.current {
color: var(--text-1); font-weight: 600;
}
.page-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.page-header h2 {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-1);
}
.btn {
display: inline-flex;
align-items: center;
gap: .3rem;
padding: .5rem 1rem;
border: 1px solid var(--line);
border-radius: var(--radius);
font-size: .85rem;
font-weight: 600;
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
text-decoration: none;
color: var(--text-1);
font-family: inherit;
background: var(--bg-2);
}
.btn:hover {
border-color: var(--blue);
}
.btn:active {
opacity: .85;
}
.btn:disabled {
opacity: .5; cursor: not-allowed; transform: none;
}
.btn-sm {
padding: .3rem .6rem; font-size: .8rem;
}
.btn-lg {
padding: .65rem 1.5rem; font-size: 1rem;
}
.btn-full {
width: 100%; justify-content: center;
}
.btn-primary {
background: var(--blue);
border-color: var(--blue);
color: #fff;
}
.btn-primary:hover {
background: var(--blue-bright); border-color: var(--blue-bright);
}
.btn-outline {
background: transparent;
border: 1px solid var(--line);
color: var(--text-2);
}
.btn-outline:hover {
border-color: var(--blue);
color: var(--blue-bright);
background: var(--blue-dim);
}
.btn-danger {
background: transparent;
border: 1px solid var(--crit);
color: var(--crit);
}
.btn-danger:hover {
background: var(--crit-dim);
}
.app-info-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.app-info-card {
background: var(--bg-1);
border-radius: var(--radius);
padding: 1.25rem;
border: 1px solid var(--line);
}
.app-info-card h3 {
margin: 0 0 .75rem 0;
font-size: .95rem;
color: var(--text-1);
}
.app-info-list {
margin: 0;
padding-left: 1.25rem;
color: var(--text-2);
font-size: .9rem;
line-height: 1.6;
}
.app-info-link {
color: var(--blue-bright);
text-decoration: none;
}
.app-info-link:hover {
text-decoration: underline;
}
.alerts-container {
margin-bottom: 1rem;
}
.alert-banner {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: var(--radius);
margin-bottom: 0.5rem;
font-size: 0.9rem;
border: 1px solid;
}
.alert-banner-info {
background: var(--blue-dim);
border-color: var(--blue-dim);
color: var(--blue-bright);
}
.alert-banner-warning {
background: var(--warn-dim);
border-color: var(--warn-dim);
color: var(--warn);
}
.alert-banner-error {
background: var(--crit-dim);
border-color: var(--crit-dim);
color: var(--crit);
}
.alert-icon {
flex-shrink: 0;
}
.alert-message {
flex: 1;
}
.alert-link {
margin-left: auto; white-space: nowrap; color: inherit; text-decoration: none;
}
.alert-link:hover {
text-decoration: underline;
}
.domain-badge {
border: 1px solid var(--line);
color: var(--text-2);
padding: .2rem .6rem;
border-radius: var(--radius);
font-size: .8rem;
font-weight: 500;
font-family: var(--font-data);
}
.empty-state {
text-align: center; padding: 3rem; color: var(--text-3);
}
.empty-state-link {
color: var(--blue-bright);
}
.form-hint {
font-size: .85rem;
color: var(--text-3);
}
.text-muted {
color: var(--text-3);
}
.launch-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
gap: 1.25rem;
margin-top: .5rem;
}
.launch-cell {
display: flex;
flex-direction: column;
align-items: center;
gap: .5rem;
text-decoration: none;
color: var(--text-1);
}
.launch-tile {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
border-radius: var(--radius);
border: 1px solid var(--line);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: transform .12s ease, border-color .12s ease;
}
.launch-cell:hover .launch-tile {
transform: translateY(-2px);
border-color: var(--blue);
}
.launch-mono {
display: none;
position: absolute;
inset: 0;
align-items: center;
justify-content: center;
color: #fff;
font-family: var(--font-ui);
font-size: 2.4rem;
font-weight: 600;
line-height: 1;
user-select: none;
}
.launch-logo {
position: relative;
width: 58%;
height: 58%;
object-fit: contain;
}
.launch-name {
max-width: 100%;
font-size: .85rem;
color: var(--text-1);
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stack-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1rem;
}
.stack-detail-card {
background: var(--bg-1);
border-radius: var(--radius);
padding: 1.25rem;
border: 1px solid var(--line);
transition: border-color 0.3s ease, transform 0.3s ease;
display: flex;
flex-direction: column;
}
.stack-detail-card:hover {
border-color: var(--blue);
transform: translateY(-2px);
}
.stack-title-text {
flex: 1;
min-width: 0;
}
.stack-title-text h3 {
margin-bottom: .25rem; overflow-wrap: anywhere;
}
.stack-detail-desc {
color: var(--text-2);
font-size: .85rem;
margin-bottom: .75rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block; font-size: .85rem; font-weight: 500; margin-bottom: .4rem; color: var(--text-1);
}
.form-control {
width: 100%;
padding: .55rem .75rem;
border: 1px solid var(--line);
border-radius: var(--radius);
font-size: .9rem;
background: var(--bg-2);
color: var(--text-1);
font-family: inherit;
transition: border-color 0.2s ease;
}
.form-control:focus {
outline: none;
border-color: var(--blue);
}
.form-control:disabled {
background: var(--bg-0);
color: var(--text-3);
cursor: not-allowed;
}
.form-row {
margin-bottom: .75rem;
}
.form-actions {
display: flex;
gap: .75rem;
margin-top: 1rem;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: .5rem;
margin-top: .25rem;
}
.required {
color: var(--crit);
}
/* ---------- shell, phone: top bar + off-canvas drawer ----------
`body` and `body.nav-open` are element-level rules the components depend on.
Dropping them is what broke the M1 phone layout. They are copied here on purpose. */
@media (max-width: 768px) {
.mobile-topbar {
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 800;
background: var(--bg-2);
border-bottom: 1px solid var(--line);
padding: .5rem 1rem;
}
.mobile-topbar-logo img {
height: 36px; width: auto; display: block;
}
.js .sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
height: 100dvh;
width: min(280px, 85vw);
transform: translateX(-100%);
transition: transform .2s ease;
z-index: 950;
border-right: 1px solid var(--line);
border-bottom: none;
}
.js .sidebar.is-open {
transform: none;
}
.js .nav-backdrop:not([hidden]) {
display: block;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .5);
z-index: 900;
}
body.shell.nav-open {
overflow: hidden;
}
.no-js .sidebar {
position: static;
height: auto;
width: 100%;
border-right: none;
border-bottom: 1px solid var(--line);
}
.no-js .nav-burger {
display: none;
}
.content {
margin-left: 0; padding: 1rem;
}
body.shell {
flex-direction: column;
}
.page-header {
flex-wrap: wrap; row-gap: .6rem;
}
}
/* ---------- shell, reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
.nav-group {
transition: none;
}
.nav-chevron {
transition: none;
}
.js .sidebar {
transition: none;
}
}
+51
View File
@@ -2376,3 +2376,54 @@ footer a:hover, footer a:focus-visible { text-decoration: underline; }
.page-index .who-mini p { text-align: center; } .page-index .who-mini p { text-align: center; }
} }
/* ================= M3 shell pages - 2026-10-11 =================
Additions only, all new class names. These are the WEBSITE's own additions to the
controller's shell (assets/shell.css, which stays a pure copy): the wordmark under
the sidebar logo, the tester button in the sidebar, the hero row, and the sample
badge. Nothing here exists in the controller, so nothing here has to keep in step
with it. See documentation/design/design-system.md "Shell". */
/* --- the website's own additions to the sidebar --- */
.sidebar-logo-link { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.sidebar-word { height: 16px; width: auto; display: block; }
.sidebar-cta { width: 100%; justify-content: center; margin-bottom: 1rem; }
/* --- every invented value on a page wears this --- */
.sample {
font-family: var(--font-data); font-size: .68rem; text-transform: uppercase;
letter-spacing: .04em; color: var(--warn);
border: 1px solid var(--warn); background: var(--warn-dim);
padding: .05rem .3rem; border-radius: var(--radius); white-space: nowrap;
}
/* --- section rhythm inside the shell's content column --- */
.shell-section { margin-bottom: 3.5rem; }
.shell-section:last-child { margin-bottom: 1rem; }
.shell .sec-head { max-width: 46rem; margin-bottom: 1.75rem; }
.shell .sec-head h2 { font-size: clamp(1.4rem, 2.6vw, 1.9rem); font-weight: 700; letter-spacing: -.02em; margin-bottom: .4rem; }
.shell .sec-head p { color: var(--text-2); font-size: 1rem; }
/* --- the hero row: copy on the left, the dashboard window on the right --- */
.hero-row { display: grid; grid-template-columns: 1fr 1.1fr; gap: 2.5rem; align-items: center; margin-bottom: 3.5rem; }
.hero-copy h1 { font-size: clamp(1.8rem, 3.4vw, 2.6rem); font-weight: 700; line-height: 1.15; letter-spacing: -.03em; margin-bottom: .75rem; }
.hero-copy p { color: var(--text-2); font-size: 1.05rem; margin-bottom: 1.5rem; max-width: 34ch; }
.hero-logo { display: flex; justify-content: center; }
.hero-logo img { max-width: 340px; width: 100%; height: auto; }
.hero-window { border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-1); overflow: hidden; }
.hero-window-bar { display: flex; align-items: center; gap: .4rem; padding: .55rem .8rem; background: var(--bg-2); border-bottom: 1px solid var(--line); }
.hw-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--line); display: inline-block; }
.hero-window-title { margin-left: .7rem; font-size: .78rem; color: var(--text-3); display: inline-flex; align-items: center; gap: .5rem; }
.hero-window-body { padding: 1.1rem; }
.hero-window-lede { font-size: .88rem; color: var(--text-2); margin-bottom: .9rem; }
.launch-grid--mini { grid-template-columns: repeat(4, 1fr); gap: .9rem; }
@media (max-width: 900px) {
.hero-row { grid-template-columns: 1fr; gap: 1.75rem; }
.hero-copy p { max-width: none; }
.launch-grid--mini { grid-template-columns: repeat(4, 1fr); }
}
@media (max-width: 600px) {
.launch-grid--mini { grid-template-columns: repeat(3, 1fr); }
.shell-section { margin-bottom: 2.5rem; }
}
+325
View File
@@ -0,0 +1,325 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script>document.documentElement.className=document.documentElement.className.replace('no-js','js');</script>
<meta name="robots" content="noindex, nofollow">
<meta name="description" content="Otthoni szerver a saját otthonodban: fényképek, dokumentumok, médiatár. 58 telepíthető alkalmazás, háromszintű automatikus mentés, magyar vagy angol nyelvű vezérlőpult. Most zárt teszt indul.">
<title>Felhom.eu — Saját felhőd, saját szabályaid | Otthoni szerver szolgáltatás</title>
<script defer src="/assets/gallery.js?v=2"></script>
<link rel="canonical" href="https://felhom.eu/">
<link rel="alternate" hreflang="hu" href="https://felhom.eu/">
<link rel="alternate" hreflang="en" href="https://felhom.eu/en/">
<link rel="alternate" hreflang="x-default" href="https://felhom.eu/">
<link rel="stylesheet" href="/assets/site.css?v=15">
<link rel="stylesheet" href="/assets/shell.css?v=15">
<link rel="icon" type="image/png" href="/assets/logo_favicon_2.svg">
<script defer src="https://stats.felhom.eu/script.js" data-website-id="d419db57-5e1c-48df-bdb3-c29f25fb1661"></script>
</head>
<body class="shell page-index">
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/assets/logo_notext_white.svg" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" id="navBurger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<span class="hamburger-box"><span class="hamburger-line"></span><span class="hamburger-line"></span><span class="hamburger-line"></span></span>
</button>
</header>
<div class="nav-backdrop" id="navBackdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<a href="/" class="sidebar-logo-link">
<img src="/assets/logo_notext_white.svg" alt="" class="sidebar-logo" aria-hidden="true">
<img src="/assets/logo-wordmark.png" alt="felhom.eu" class="sidebar-word">
</a>
</div>
<ul class="nav-links">
<li><a href="/" class="active"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-home"/></svg>Kezdőlap</a></li>
<li><a href="/#szolgaltatasok"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-rocket"/></svg>Szolgáltatások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-apps"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-package"/></svg>Alkalmazások<svg class="ico nav-chevron" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-chevron-down"/></svg></button>
<ul id="nav-group-apps" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/alkalmazasok">Minden alkalmazás</a></li>
<li><a href="/alkalmazasok#fajlkezeles-irodai">Fájlkezelés &amp; Irodai</a></li>
<li><a href="/alkalmazasok#media-szorakozas">Média &amp; Szórakozás</a></li>
<li><a href="/alkalmazasok#otthon-eletmod">Otthon &amp; Életmód</a></li>
<li><a href="/alkalmazasok#produktivitas-tudaskezeles">Produktivitás &amp; Tudáskezelés</a></li>
<li><a href="/alkalmazasok#rendszer-fejlesztes">Rendszer &amp; Fejlesztés</a></li>
</ul>
</li>
<li><a href="/technologiak"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-layers"/></svg>Technológiák</a></li>
<li><a href="/biztonsagimentes"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-shield-check"/></svg>Biztonsági mentés</a></li>
</ul>
<div class="sidebar-bottom">
<a href="/teszteles" class="btn btn-primary sidebar-cta">Jelentkezem tesztelőnek →</a>
<div class="nav-group-label">A Felhomról</div>
<ul class="nav-links nav-links-sub">
<li><a href="/#hogyan-mukodik">Hogyan működik</a></li>
<li><a href="/gyik">GYIK</a></li>
<li><a href="/kapcsolat">Kapcsolat</a></li>
</ul>
<div class="sidebar-footer">
<details class="lang-globe"><summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-globe"/></svg></summary><ul class="lang-globe-menu"><li><a href="/" lang="hu" hreflang="hu" class="lang-globe-item current" aria-current="true">Magyar</a></li><li><a href="/en/" lang="en" hreflang="en" class="lang-globe-item">English</a></li></ul></details>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<h2>Saját felhőd, saját szabályaid</h2>
<span class="domain-badge">Otthoni szerver szolgáltatás</span>
</div>
<section class="hero-row">
<div class="hero-copy">
<h1>Egy kis gép az otthonodban.</h1>
<p>A fényképeid, a dokumentumaid, a filmjeid. Minden éjjel mentés készül. Mi figyeljük, te használod.</p>
<a href="/teszteles" class="btn btn-primary btn-lg">Jelentkezem tesztelőnek →</a>
</div>
<div class="hero-window" aria-label="A vezérlőpult képe">
<div class="hero-window-bar">
<span class="hw-dot"></span><span class="hw-dot"></span><span class="hw-dot"></span>
<span class="hero-window-title">otthonod.hu <span class="sample">minta</span></span>
</div>
<div class="hero-window-body">
<p class="hero-window-lede">Így néz ki a doboz vezérlőpultja.</p>
<div class="launch-grid launch-grid--mini">
<a class="launch-cell" href="/alkalmazasok#immich">
<span class="launch-tile" style="background: hsl(18, 45%, 38%)"><span class="launch-mono">I</span><img class="launch-logo" src="/assets/immich-logo.svg" alt="" loading="lazy"></span>
<span class="launch-name">Immich</span>
</a>
<a class="launch-cell" href="/alkalmazasok#jellyfin">
<span class="launch-tile" style="background: hsl(348, 45%, 38%)"><span class="launch-mono">J</span><img class="launch-logo" src="/assets/jellyfin-logo.svg" alt="" loading="lazy"></span>
<span class="launch-name">Jellyfin</span>
</a>
<a class="launch-cell" href="/alkalmazasok#nextcloud">
<span class="launch-tile" style="background: hsl(201, 45%, 38%)"><span class="launch-mono">N</span><img class="launch-logo" src="/assets/nextcloud-logo.svg" alt="" loading="lazy"></span>
<span class="launch-name">Nextcloud</span>
</a>
<a class="launch-cell" href="/alkalmazasok#vaultwarden">
<span class="launch-tile" style="background: hsl(66, 45%, 38%)"><span class="launch-mono">V</span><img class="launch-logo" src="/assets/vaultwarden-logo.svg" alt="" loading="lazy"></span>
<span class="launch-name">Vaultwarden</span>
</a>
</div>
</div>
</div>
</section>
<section class="shell-section" id="szolgaltatasok">
<div class="sec-head">
<h2>Mit tud a doboz ma?</h2>
<p>Nem terv és nem ígéret: ezek ma működő funkciók egy éles Felhom dobozon.</p>
</div>
<div class="caps">
<div class="caps-group">
<h3>Amit nap mint nap használsz</h3>
<ul class="caps-list">
<li class="caps-item">
<h4><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-camera"/></svg> Fényképek, dokumentumok, média</h4>
<p>A telefonod fotói automatikusan feltöltődnek a saját gépedre. A beszkennelt papírjaidat a rendszer felismeri és keresővé teszi. A filmjeid, zenéid böngészőből és mobil appból is mennek.</p>
</li>
<li class="caps-item">
<h4><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-folder"/></svg> Hálózati megosztás</h4>
<p>A doboz mappái megjelennek a Windows Intézőben és a Mac Finderben: úgy húzod rájuk a fájlokat, mint bármelyik másik mappára. Megosztásonként beállítható, hogy csak olvasható legyen.</p>
</li>
<li class="caps-item">
<h4><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-package"/></svg> 58 telepíthető alkalmazás</h4>
<p>A vezérlőpultból telepíthetők, pár kattintással, parancssor nélkül. Szinte mind nyílt forráskódú, és mind a te gépeden fut.</p>
</li>
<li class="caps-item">
<h4><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-globe"/></svg> Magyarul vagy angolul</h4>
<p>A vezérlőpult magyarul és angolul is használható. A nyelvet a háztartás választja, és a dobozról érkező e-mailek is ezt a nyelvet követik.</p>
</li>
</ul>
</div>
<div class="caps-group">
<h3>Ami a háttérben véd</h3>
<ul class="caps-list">
<li class="caps-item">
<h4><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-shield-check"/></svg> Háromszintű automatikus mentés</h4>
<p>Minden éjjel készül egy mentés a dobozon, egy másodpéldány egy második meghajtóra (ha van), és egy titkosított másolat egy távoli tárhelyre. A megosztott mappáid a második meghajtón és a távoli másolatban is benne vannak.</p>
</li>
<li class="caps-item">
<h4><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-lock"/></svg> Helyreállítási kód, amit csak te ismersz</h4>
<p>A távoli mentésed titkosításának kulcsa egy helyreállítási kód, ami pontosan egyszer jelenik meg: a te képernyődön. Mi nem ismerjük, és nem tudjuk visszaállítani. Ez azt is jelenti, hogy a kód nélkül a távoli mentés nekünk sem olvasható: tedd el jól.</p>
</li>
<li class="caps-item">
<h4><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-rotate-cw"/></svg> Visszaállítás, ha baj van</h4>
<p>Egy törölt fájl visszahozható a mentésből. Ha maga a doboz megy tönkre, az újratelepített rendszerre a titkosított távoli mentésből visszahozhatók az alkalmazásaid adatai, a helyreállítási kóddal.</p>
</li>
<li class="caps-item">
<h4><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-home"/></svg> Internetkimaradáskor is működik</h4>
<p>A doboz az otthonodban van. Ha elmegy a net, az otthoni hálózaton továbbra is eléred a fotóidat és a fájljaidat: nem egy távoli szolgáltató engedélyétől függenek.</p>
</li>
</ul>
</div>
<div class="caps-group">
<h3>Amiről mi gondoskodunk</h3>
<ul class="caps-list">
<li class="caps-item">
<h4><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-stethoscope"/></svg> Távoli felügyelet, automatikus frissítés</h4>
<p>Figyeljük a doboz állapotát. A vezérlőpult magától frissül a legutóbbi kiadásra, és a doboz rendszere is megkapja a biztonsági frissítéseket. Neked nem kell csinálnod semmit. Ha valami elromlik, mi látjuk.</p>
</li>
<li class="caps-item">
<h4><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-rotate-cw"/></svg> Óvatos alkalmazásfrissítés</h4>
<p>Egy alkalmazás új verziója csak akkor jut el hozzád, ha előbb kipróbáltuk. Frissítés előtt friss mentés készül, és ha az új verzió nem indul el rendesen, a doboz magától visszaállítja az előzőt, az adataiddal együtt.</p>
</li>
</ul>
</div>
</div>
</section>
<section class="shell-section" id="vezerlopult">
<div class="sec-head">
<h2>Így néz ki a vezérlőpult</h2>
<p>Valódi képek egy Felhom dobozról. Kattints rájuk a teljes mérethez.</p>
</div>
<div class="dash-grid">
<figure class="dash-shot">
<a href="/assets/dashboard-start-hu.webp" data-gallery="dashboard"><img src="/assets/dashboard-start-hu.webp" alt="A vezérlőpult Indítópultja: az otthon telepített alkalmazások csempéi" width="1440" height="900" loading="lazy"></a>
<figcaption><strong>Indítópult</strong>: minden alkalmazásod egy kattintásra.</figcaption>
</figure>
<figure class="dash-shot">
<a href="/assets/dashboard-apps-hu.webp" data-gallery="dashboard"><img src="/assets/dashboard-apps-hu.webp" alt="Az Alkalmazások oldal: telepített és telepíthető alkalmazások kártyái, állapottal" width="1440" height="900" loading="lazy"></a>
<figcaption><strong>Alkalmazások</strong>: telepítés pár kattintással, és látod, mi fut és mi naprakész.</figcaption>
</figure>
<figure class="dash-shot">
<a href="/assets/dashboard-app-hu.webp" data-gallery="dashboard"><img src="/assets/dashboard-app-hu.webp" alt="Egy alkalmazás saját oldala: mire jó, képek, és az első lépések" width="1440" height="900" loading="lazy"></a>
<figcaption><strong>Egy alkalmazás oldala</strong>: mire jó, hova tedd a fájlokat, és hogyan kezdj hozzá.</figcaption>
</figure>
<figure class="dash-shot">
<a href="/assets/dashboard-backups-hu.webp" data-gallery="dashboard"><img src="/assets/dashboard-backups-hu.webp" alt="A mentések oldala: minden alkalmazás adatbázisának éjszakai mentése, ellenőrizve" width="1440" height="900" loading="lazy"></a>
<figcaption><strong>Mentések</strong>: minden éjjel minden alkalmazás adatbázisa mentésre kerül, és ellenőrizzük.</figcaption>
</figure>
</div>
</section>
<section class="shell-section" id="hogyan-mukodik">
<div class="sec-head">
<h2>Hogyan működik?</h2>
<p>Az első beszélgetéstől a működő rendszerig.</p>
</div>
<div class="flow">
<div class="flow-step">
<h3>Egyeztetés</h3>
<p>Megbeszéljük az igényeidet: milyen alkalmazásokra van szükséged, és mekkora tárhelyet szeretnél.</p>
</div>
<div class="flow-step">
<h3>Hardver kiválasztás</h3>
<p>Segítünk megtalálni a legjobb ár-érték arányú gépet, ami az igényeidnek megfelel.</p>
</div>
<div class="flow-step">
<h3>Telepítés</h3>
<p>Feltelepítjük és bekonfiguráljuk az alkalmazásokat, a mentéseket, és a monitoringot.</p>
</div>
<div class="flow-step">
<h3>Átadás</h3>
<p>Bemutatjuk a rendszert, segítünk áthozni a meglévő adataidat, és válaszolunk minden kérdésedre.</p>
</div>
<div class="flow-step">
<h3>Üzemeltetés</h3>
<p>Figyeljük a rendszert, frissítünk, mentéseket ellenőrzünk. Te csak használod.</p>
</div>
</div>
</section>
<section class="shell-section" id="miert">
<div class="sec-head">
<h2>Miért a sajátod?</h2>
<p>Ugyanaz a kényelem, más feltételekkel.</p>
</div>
<div class="vs">
<div class="vs-head">
<div>Felhőszolgáltatónál</div>
<div>Saját dobozon</div>
</div>
<div class="vs-row">
<div>Havonta fizetsz a tárhelyért és a prémium funkciókért, és az árak csak nőnek idővel.</div>
<div><strong>A gép a tiéd, örökre.</strong> Nincs tárhely-előfizetés, ami évről évre drágul: az áramot fizeted, és ha kéred, az üzemeltetést.</div>
</div>
<div class="vs-row">
<div>A fotóid, dokumentumaid, személyes adataid idegen szervereken vannak, idegen kezekben.</div>
<div><strong>Az adataid az otthonodban maradnak</strong>, nem a Google vagy a Microsoft szerverein. A távoli mentésbe csak titkosítva kerülnek.</div>
</div>
<div class="vs-row">
<div>A tárhely fogy, és ha betelt, megint fizetsz.</div>
<div><strong>Nincs 15GB-os limit.</strong> Bővítheted ahogy nőnek az igényeid, TB-okkal, ha kell.</div>
</div>
<div class="vs-row">
<div>Nem te döntöd el, milyen szolgáltatásokat használsz, és mikor szűnnek meg.</div>
<div><strong>Te döntöd el, milyen szolgáltatásokat futtatsz.</strong> Te vagy a főnök, nem a szolgáltató.</div>
</div>
</div>
<div class="band">
<div class="band-item">
<h3>Amit telepíthetsz</h3>
<p>Fájlkezelés, fotók és média, dokumentumok, jelszókezelés, okosotthon, fejlesztői eszközök. Nextcloud, Immich, Jellyfin, Paperless-ngx, Vaultwarden, Home Assistant és még 52 másik: <strong>58 alkalmazás</strong>, szinte mind nyílt forráskódú.</p>
<a href="/alkalmazasok" class="btn btn-outline">Összes alkalmazás megtekintése →</a>
</div>
<div class="band-item">
<h3>Ami alatta fut</h3>
<p>Docker konténerek egy Proxmox alapon, háromszintű mentés, titkosított alagút nyitott port nélkül, magyar vagy angol vezérlőpult. Az igényeidhez igazítjuk, a <strong>legegyszerűbbtől a legkomolyabbig</strong>.</p>
<a href="/technologiak" class="btn btn-outline">Technológiákról bővebben →</a>
</div>
</div>
</section>
<section class="shell-section" id="zart-teszt">
<div class="sec-head">
<h2>Zárt teszt</h2>
<p>Néhány magyar háztartással most kezdjük el élesben használni a rendszert.</p>
</div>
<div class="app-info-grid">
<div class="app-info-card">
<h3>Valódi használat</h3>
<p>Nem demó: a doboz nálad marad, és az igazi fényképeidet, dokumentumaidat teszed rá. Segítünk a telepítésben és az átállásban, utána te használod a mindennapokban.</p>
</div>
<div class="app-info-card">
<h3>Visszajelzés</h3>
<p>Cserébe azt kérjük, hogy mondd el, mi zavaró, mi érthetetlen, mi nem működik. Egy „ezt a gombot nem találtam" ér nekünk a legtöbbet: abból tudjuk, mit kell kijavítani.</p>
</div>
</div>
<div class="alerts-container">
<div class="alert-banner alert-banner-warning">
<span class="alert-icon"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-triangle-alert"/></svg></span>
<span class="alert-message"><strong>Ismert korlátok, őszintén.</strong> A vezérlőpulthoz jelenleg <strong>egy közös háztartási jelszó</strong> tartozik, külön családtagi fiókok még nincsenek (az egyes alkalmazásoknak viszont saját felhasználóik lehetnek). A <strong>TV-re streamelés (DLNA)</strong> még nincs: egyelőre böngészőből és mobil appból megy a médiatár. És mivel ez teszt, számíts arra, hogy találsz hibát: ez a lényege.</span>
</div>
</div>
<div class="close-cta">
<p>Ha ez érdekel, írj. <strong>Nem kötelez semmire</strong>, először csak beszélgetünk.</p>
<a href="/teszteles" class="btn btn-primary btn-lg">Jelentkezem tesztelőnek →</a>
<div class="who-mini">
<img src="/assets/operator-portrait-small.webp" alt="Nagyfenyvesi Viktor, a Felhom üzemeltetője" width="160" height="200" loading="lazy">
<p>A Felhom mögött egy ember áll: <strong>Nagyfenyvesi Viktor</strong>. Ha írsz, ő válaszol. <a href="/kapcsolat#ki-all-mogotte">Ki áll mögötte →</a></p>
</div>
</div>
</section>
</main>
<script>
/* The controller's drawer, same behaviour: burger toggles, backdrop and Escape close.
The no-js -> js flip in <head> is what arms the off-canvas CSS. */
(function(){
var b=document.getElementById('navBurger'),s=document.getElementById('sidebar'),k=document.getElementById('navBackdrop');
if(!b||!s||!k)return;
function open(){s.classList.add('is-open');k.hidden=false;document.body.classList.add('nav-open');b.setAttribute('aria-expanded','true');}
function close(){s.classList.remove('is-open');k.hidden=true;document.body.classList.remove('nav-open');b.setAttribute('aria-expanded','false');}
b.addEventListener('click',function(){s.classList.contains('is-open')?close():open();});
k.addEventListener('click',close);
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&s.classList.contains('is-open')){close();b.focus();}});
s.querySelectorAll('a').forEach(function(l){l.addEventListener('click',close);});
window.addEventListener('resize',function(){if(window.innerWidth>768)close();});
})();
/* collapsible sidebar group, copied from the controller's layout.html */
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var g=btn.closest('.nav-group');if(!g)return;
var willOpen=!g.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(x){
x.classList.remove('is-open');
var t=x.querySelector('.nav-group-toggle');if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){g.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
</script>
</body>
</html>