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
+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; }
}
/* ================= 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; }
}