website: M3 part A - the controller's shell, and the home page inside it
gates / gates (push) Successful in 6m58s
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:
@@ -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
@@ -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):
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 & Irodai</a></li>
|
||||||
|
<li><a href="/alkalmazasok#media-szorakozas">Média & Szórakozás</a></li>
|
||||||
|
<li><a href="/alkalmazasok#otthon-eletmod">Otthon & Életmód</a></li>
|
||||||
|
<li><a href="/alkalmazasok#produktivitas-tudaskezeles">Produktivitás & Tudáskezelés</a></li>
|
||||||
|
<li><a href="/alkalmazasok#rendszer-fejlesztes">Rendszer & 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>
|
||||||
Reference in New Issue
Block a user