@hubpav/hwio-ui 0.7.11 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/package.json +4 -4
  2. package/src/components/HWioApplicationTile.astro +49 -0
  3. package/src/components/HWioButton.astro +9 -4
  4. package/src/components/HWioChoiceCards.astro +3 -3
  5. package/src/components/HWioContactSection.astro +1 -1
  6. package/src/components/HWioCookieConsent.astro +9 -9
  7. package/src/components/HWioCutout.astro +58 -0
  8. package/src/components/HWioFaq.astro +1 -1
  9. package/src/components/HWioFeatureCard.astro +1 -1
  10. package/src/components/HWioFooter.astro +7 -4
  11. package/src/components/HWioHeader.astro +13 -6
  12. package/src/components/HWioHeroSplit.astro +14 -7
  13. package/src/components/HWioIsoScene.astro +93 -0
  14. package/src/components/HWioKeyFigure.astro +45 -0
  15. package/src/components/HWioLanguageSwitcher.astro +11 -1
  16. package/src/components/HWioLogoStrip.astro +1 -1
  17. package/src/components/HWioMegaLinks.astro +54 -0
  18. package/src/components/HWioPhoto.astro +36 -0
  19. package/src/components/HWioPricingTiers.astro +1 -1
  20. package/src/components/HWioProcessSteps.astro +1 -1
  21. package/src/components/HWioReveal.astro +15 -0
  22. package/src/components/HWioSection.astro +1 -0
  23. package/src/components/HWioStatsBand.astro +2 -2
  24. package/src/components/HWioStoryCard.astro +38 -0
  25. package/src/styles/prose.css +3 -3
  26. package/src/styles/styles.css +325 -37
  27. package/src/styles/themes/er3o.css +92 -1
  28. package/src/styles/themes/hwio-dark.css +127 -19
  29. package/src/styles/themes/hwio-forestry-dark.css +127 -19
  30. package/src/styles/themes/hwio-forestry.css +121 -13
  31. package/src/styles/themes/hwio.css +122 -14
  32. package/src/types.ts +9 -0
@@ -0,0 +1,54 @@
1
+ ---
2
+ /**
3
+ * A group of links for HWioHeader's `mega:<key>` panels (owner, 2026-10-30): an optional caps title
4
+ * with an "all" link on its line, then links with a one-line description. Labels never wrap; the
5
+ * descriptions are small (13 px), Regular, Gray and balanced, so siblings read as equals. `columns` is
6
+ * fixed: the panel shows only from the header's desktop breakpoint, and a column count that grows with
7
+ * the viewport (three at 2xl) squeezed descriptions into broken lines. Products may carry a thumbnail
8
+ * (a cut-out with the shared product shadow).
9
+ */
10
+ import type { HWioLinkItem } from '../types';
11
+ import HWioIcon from './HWioIcon.astro';
12
+ interface Item extends HWioLinkItem {
13
+ description?: string;
14
+ /** A product thumbnail (cut-out on a transparent ground); decorative, the label names the product. */
15
+ image?: string;
16
+ }
17
+ interface Props {
18
+ title?: string;
19
+ all?: HWioLinkItem;
20
+ items: Item[];
21
+ columns?: 1 | 2 | 3;
22
+ /** data-track-location for the links (default "header-menu"). */
23
+ trackLocation?: string;
24
+ class?: string;
25
+ }
26
+ const { title, all, items, columns = 1, trackLocation = 'header-menu', class: className = '' } = Astro.props;
27
+ const COLUMNS = { 1: 'grid-cols-1', 2: 'grid-cols-2', 3: 'grid-cols-3' } as const;
28
+ const linkAttrs = (item: HWioLinkItem) => (item.external ? { target: '_blank', rel: 'noopener noreferrer' } : {});
29
+ ---
30
+ <div class:list={['min-w-0', className]}>
31
+ {(title || all) && (
32
+ <div class="mb-4 flex items-baseline justify-between gap-6 px-3">
33
+ {title && <p class="hwio-mega-title">{title}</p>}
34
+ {all && <a href={all.href} {...linkAttrs(all)} class="hwio-link hwio-weight-label whitespace-nowrap text-sm" data-track={all.track?.id} data-track-location={all.track?.location ?? trackLocation}>{all.label}</a>}
35
+ </div>
36
+ )}
37
+ <ul class:list={['grid gap-x-6 gap-y-1', COLUMNS[columns]]}>
38
+ {items.map((item) => (
39
+ <li class="min-w-0">
40
+ <a href={item.href} {...linkAttrs(item)} class="flex items-start gap-4 rounded-xl px-3 py-2.5 transition-colors hover:bg-base-200" aria-current={item.current ? 'page' : undefined} data-track={item.track?.id} data-track-location={item.track?.location ?? trackLocation}>
41
+ {item.image && (
42
+ <span class="flex h-20 w-16 shrink-0 items-center justify-center">
43
+ <img src={item.image} alt="" loading="lazy" decoding="async" class="hwio-product-shadow max-h-full max-w-full object-contain" />
44
+ </span>
45
+ )}
46
+ <span class="min-w-0 flex-1">
47
+ <span class="hwio-mega-label">{item.label}{item.external && <HWioIcon name="arrow-up-right" size="sm" class="opacity-50" />}</span>
48
+ {item.description && <span class="hwio-menu-desc mt-0.5 block">{item.description}</span>}
49
+ </span>
50
+ </a>
51
+ </li>
52
+ ))}
53
+ </ul>
54
+ </div>
@@ -0,0 +1,36 @@
1
+ ---
2
+ /**
3
+ * A photograph at a fixed aspect ratio, filling its frame (object-fit: cover; `focus` picks the
4
+ * part that stays in view). Photographs come from the artwork library, clearance public (Brand
5
+ * Guidelines chapter 08: photographs prove). For a product on a transparent ground use HWioCutout.
6
+ */
7
+ import type { HWioAspect, HWioMedia } from '../types';
8
+ interface Props {
9
+ image: HWioMedia;
10
+ aspect?: HWioAspect;
11
+ rounded?: 'none' | 'xl' | '2xl';
12
+ focus?: 'center' | 'top' | 'bottom' | 'left' | 'right';
13
+ /** Above the fold: load at once with high priority. */
14
+ eager?: boolean;
15
+ class?: string;
16
+ imgClass?: string;
17
+ }
18
+ const { image, aspect = '4/3', rounded = '2xl', focus = 'center', eager = false, class: className = '', imgClass = '' } = Astro.props;
19
+ const ASPECT = { '1/1': 'aspect-square', '4/3': 'aspect-[4/3]', '3/2': 'aspect-[3/2]', '16/9': 'aspect-video', '4/5': 'aspect-[4/5]', '3/4': 'aspect-[3/4]' } as const;
20
+ const ROUNDED = { none: 'rounded-none', xl: 'rounded-xl', '2xl': 'rounded-2xl' } as const;
21
+ const FOCUS = { center: 'object-center', top: 'object-top', bottom: 'object-bottom', left: 'object-left', right: 'object-right' } as const;
22
+ ---
23
+ <div class:list={['relative overflow-hidden bg-base-200', ASPECT[aspect], ROUNDED[rounded], className]}>
24
+ <img
25
+ src={image.src}
26
+ srcset={image.srcset}
27
+ sizes={image.sizes}
28
+ alt={image.alt}
29
+ width={image.width}
30
+ height={image.height}
31
+ loading={eager ? 'eager' : 'lazy'}
32
+ decoding="async"
33
+ fetchpriority={eager ? 'high' : undefined}
34
+ class:list={['absolute inset-0 h-full w-full object-cover', FOCUS[focus], imgClass]}
35
+ />
36
+ </div>
@@ -26,7 +26,7 @@ const cols = (Math.min(Math.max(tiers.length, 1), 4) as 1 | 2 | 3 | 4);
26
26
  )}
27
27
  {tier.description && <p class="opacity-80">{tier.description}</p>}
28
28
  <ul class="my-4 space-y-2 text-sm">
29
- {tier.features.map((f) => <li class="flex items-start gap-2"><HWioIcon name="check" size="sm" class="mt-0.5 shrink-0 text-accent" /><span>{f}</span></li>)}
29
+ {tier.features.map((f) => <li class="flex items-start gap-2"><HWioIcon name="check" size="sm" class="hwio-icon mt-0.5 shrink-0" /><span>{f}</span></li>)}
30
30
  </ul>
31
31
  {tier.cta && <div class="card-actions mt-auto"><HWioButton href={tier.cta.href} external={tier.cta.external} variant={tier.featured ? 'primary' : 'secondary'} class="w-full" track={tier.cta.track}>{tier.cta.label}</HWioButton></div>}
32
32
  </div>
@@ -8,7 +8,7 @@ const LAYOUT = { horizontal: 'grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols
8
8
  <ol class:list={[LAYOUT[orientation], className]} data-hwio-steps>
9
9
  {items.map((step, i) => (
10
10
  <li class="flex gap-4">
11
- <span class="badge badge-primary badge-lg shrink-0 font-semibold tabular-nums" aria-hidden="true">{step.number ?? i + 1}</span>
11
+ <span class="badge badge-primary badge-lg hwio-weight-label shrink-0 tabular-nums" aria-hidden="true">{step.number ?? i + 1}</span>
12
12
  <div>
13
13
  <h3 class="text-lg font-semibold"><span class="sr-only">{step.number ?? i + 1}. </span>{step.title}</h3>
14
14
  <p class="mt-1 opacity-80">{step.text}</p>
@@ -0,0 +1,15 @@
1
+ ---
2
+ /**
3
+ * Starts the scroll reveal on the page (@hubpav/hwio-web-runtime/reveal). Render it once, in the layout
4
+ * of pages that mark content with `data-hwio-reveal` (HWioSection `reveal`, HWioApplicationTile
5
+ * `reveal`, a site's own markup). Renders nothing.
6
+ *
7
+ * Fail-safe: content waiting for its reveal hides only after this runtime has started; without it the
8
+ * content simply shows. Each element fades in and settles once, the first time it enters the viewport;
9
+ * `data-hwio-reveal-delay="<ms>"` staggers siblings. Under prefers-reduced-motion everything shows at once.
10
+ */
11
+ ---
12
+ <script>
13
+ import { hwioRevealRuntime } from '@hubpav/hwio-web-runtime/reveal';
14
+ hwioRevealRuntime();
15
+ </script>
@@ -1,4 +1,5 @@
1
1
  ---
2
+ /** Section with the system rhythm, tone and container. `reveal` fades it in once in view; the reveal runs only with HWioReveal on the page (fail-safe: without it the section simply shows). */
2
3
  import type { HWioTone } from '../types';
3
4
  interface Props { id?: string; tone?: HWioTone; spacing?: 'normal' | 'compact' | 'none'; container?: boolean; as?: 'section' | 'div' | 'article'; reveal?: boolean; class?: string; [key: string]: unknown }
4
5
  const TONE = { base: 'bg-base-100 text-base-content hwio-tone-base', alt: 'bg-base-200 text-base-content hwio-tone-alt', neutral: 'bg-neutral text-neutral-content hwio-tone-neutral' } as const;
@@ -11,10 +11,10 @@ const cols = COLS[(Math.min(Math.max(items.length, 2), 5) as 2 | 3 | 4 | 5)];
11
11
  const numeric = typeof s.value === 'number';
12
12
  return (
13
13
  <div class:list={['px-8 py-2', i > 0 ? 'sm:border-l sm:border-base-300' : '']}>
14
- <div class="mb-2.5 text-[2.25rem] font-bold leading-none tracking-[-0.03em] lg:text-[2.5rem]">
14
+ <div class="hwio-figure mb-2.5">
15
15
  <span class="hwio-stat-value" data-hwio-counter={animate && numeric ? 'true' : undefined} data-target={numeric ? String(s.value) : undefined} data-suffix={s.suffix ?? ''}>{s.value}{s.suffix ?? ''}</span>
16
16
  </div>
17
- <div class="text-[15px] font-semibold">{s.label}</div>
17
+ <div class="hwio-weight-label text-[15px]">{s.label}</div>
18
18
  {s.description && <div class="hwio-muted mt-0.5 text-sm">{s.description}</div>}
19
19
  </div>
20
20
  );
@@ -0,0 +1,38 @@
1
+ ---
2
+ /**
3
+ * Customer-story card: a field photograph under the guidelines' scrim (chapter 08), the customer as a
4
+ * caps kicker and what they built as the title, White type over the dark, calm bottom of the frame;
5
+ * the whole card links to the case study (its title underlines on hover and focus, the arrow nudges).
6
+ * Pass the photograph as `image`, or your own picture in the `media` slot. Logos stay out: the card
7
+ * names the customer in text (the kicker) only when consent or a published case study allows it.
8
+ */
9
+ import type { HWioAspect, HWioMedia, HWioTrack } from '../types';
10
+ import HWioPhoto from './HWioPhoto.astro';
11
+ interface Props {
12
+ href: string;
13
+ kicker: string;
14
+ title: string;
15
+ /** The link line under the title ("Read the story"). */
16
+ linkLabel: string;
17
+ image?: HWioMedia;
18
+ aspect?: HWioAspect;
19
+ focus?: 'center' | 'top' | 'bottom' | 'left' | 'right';
20
+ headingLevel?: 'h2' | 'h3' | 'h4';
21
+ track?: HWioTrack;
22
+ eager?: boolean;
23
+ class?: string;
24
+ }
25
+ const { href, kicker, title, linkLabel, image, aspect = '4/5', focus = 'center', headingLevel = 'h3', track, eager = false, class: className = '' } = Astro.props;
26
+ const Heading = headingLevel;
27
+ ---
28
+ <a href={href} class:list={['hwio-card-link group relative block overflow-hidden rounded-2xl text-white', className]} data-track={track?.id} data-track-location={track?.location}>
29
+ <slot name="media">
30
+ {image && <HWioPhoto image={image} aspect={aspect} focus={focus} rounded="2xl" eager={eager} imgClass="transition-transform duration-500 ease-out group-hover:scale-[1.02] motion-reduce:transition-none" />}
31
+ </slot>
32
+ <div class="hwio-scrim pointer-events-none absolute inset-0 rounded-2xl"></div>
33
+ <div class="absolute inset-x-0 bottom-0 p-6 lg:p-7">
34
+ <p class="hwio-kicker text-[var(--hwio-kicker-on-neutral,#d1d5db)]">{kicker}</p>
35
+ <Heading class="hwio-role-title hwio-card-title mt-2 text-white">{title}</Heading>
36
+ <p class="hwio-weight-label mt-3 text-[0.9375rem] text-white">{linkLabel}<span aria-hidden="true" class="hwio-arrow ml-1.5">→</span></p>
37
+ </div>
38
+ </a>
@@ -9,8 +9,8 @@
9
9
  .hwio-prose ol { list-style: decimal; padding-left: calc(var(--spacing) * 6); margin-bottom: calc(var(--spacing) * 4); }
10
10
  .hwio-prose li { padding-left: calc(var(--spacing) * 1); margin-bottom: calc(var(--spacing) * 2); }
11
11
  .hwio-prose a { color: var(--hwio-link, var(--color-secondary)); text-decoration: underline; text-underline-offset: 0.2em; }
12
- .hwio-prose a:hover { color: var(--color-primary); }
13
- .hwio-prose strong { font-weight: 600; }
12
+ .hwio-prose a:hover { color: var(--hwio-link-hover, var(--color-primary)); }
13
+ .hwio-prose strong { font-weight: var(--hwio-weight-strong, 600); }
14
14
  .hwio-prose blockquote { border-left: 4px solid var(--color-secondary); padding-left: calc(var(--spacing) * 4); font-style: italic; margin-block: calc(var(--spacing) * 6); }
15
15
  .hwio-prose code { background: var(--color-base-200); padding: 0.125em 0.375em; border-radius: var(--radius-field); font-size: 0.9em; }
16
16
  .hwio-prose pre { background: var(--color-neutral); color: var(--color-neutral-content); padding: calc(var(--spacing) * 4); border-radius: var(--radius-box); overflow-x: auto; margin-block: calc(var(--spacing) * 6); }
@@ -18,7 +18,7 @@
18
18
  .hwio-prose hr { border-color: var(--color-base-300); margin-block: calc(var(--spacing) * 8); }
19
19
  .hwio-prose img { border-radius: var(--radius-box); margin-block: calc(var(--spacing) * 6); margin-inline: auto; max-width: 100%; }
20
20
  .hwio-prose table { width: 100%; border-collapse: collapse; margin-block: calc(var(--spacing) * 6); }
21
- .hwio-prose th { background: var(--color-base-200); border: 1px solid var(--color-base-300); padding: calc(var(--spacing) * 2) calc(var(--spacing) * 4); text-align: left; font-weight: 600; }
21
+ .hwio-prose th { background: var(--color-base-200); border: 1px solid var(--color-base-300); padding: calc(var(--spacing) * 2) calc(var(--spacing) * 4); text-align: left; font-weight: var(--hwio-weight-label, 600); }
22
22
  .hwio-prose td { border: 1px solid var(--color-base-300); padding: calc(var(--spacing) * 2) calc(var(--spacing) * 4); }
23
23
  .hwio-prose-lg { font-size: var(--text-lg); }
24
24
  }
@@ -57,30 +57,25 @@
57
57
  overflow-x: hidden;
58
58
  background-color: var(--color-base-100);
59
59
  color: var(--color-base-content);
60
- font-size: 1.0625rem; /* 17 px body (direction A) */
61
- line-height: 1.6;
60
+ font-size: var(--hwio-type-body-size, 1.0625rem);
61
+ line-height: var(--hwio-type-body-leading, 1.6);
62
62
  }
63
63
  button:not(:disabled),
64
64
  [role='button']:not(:disabled) {
65
65
  cursor: pointer;
66
66
  }
67
- /* Type ramp (direction A): 56 / 40 / 28 / 22 / 17 on large screens, tighter tracking as size grows. */
67
+ /* Type ramp from the theme's type preset (--hwio-type-<role>-*, generated from brand.json with
68
+ the responsive steps as media queries). HARDWARIO themes: the Brand Guidelines' web scale
69
+ (H1 60-72 px Bold, H2 40-48 px and H3 28-32 px Semibold, line heights 1.2 / 1.3 / 1.4 at
70
+ least). er3o keeps direction A. The fallbacks are direction A's phone values. */
68
71
  h1, h2, h3, h4, h5, h6 {
69
- font-weight: 700;
72
+ font-weight: var(--hwio-type-title-weight, 700);
70
73
  text-wrap: balance;
71
74
  }
72
- h1 { font-size: 2.5rem; line-height: 1.05; letter-spacing: -0.03em; }
73
- h2 { font-size: 2rem; line-height: 1.1; letter-spacing: -0.025em; }
74
- h3 { font-size: 1.375rem; line-height: 1.2; letter-spacing: -0.01em; }
75
- h4 { font-size: 1.125rem; line-height: 1.3; letter-spacing: -0.005em; }
76
- @media (width >= 48rem) {
77
- h1 { font-size: 3rem; }
78
- h2 { font-size: 2.5rem; }
79
- h3 { font-size: 1.375rem; }
80
- }
81
- @media (width >= 64rem) {
82
- h1 { font-size: 3.5rem; }
83
- }
75
+ h1 { font-size: var(--hwio-type-display-size, 2.5rem); font-weight: var(--hwio-type-display-weight, 700); line-height: var(--hwio-type-display-leading, 1.05); letter-spacing: var(--hwio-type-display-tracking, -0.03em); }
76
+ h2 { font-size: var(--hwio-type-heading-size, 2rem); font-weight: var(--hwio-type-heading-weight, 700); line-height: var(--hwio-type-heading-leading, 1.1); letter-spacing: var(--hwio-type-heading-tracking, -0.025em); }
77
+ h3 { font-size: var(--hwio-type-subheading-size, 1.375rem); font-weight: var(--hwio-type-subheading-weight, 700); line-height: var(--hwio-type-subheading-leading, 1.2); letter-spacing: var(--hwio-type-subheading-tracking, -0.01em); }
78
+ h4 { font-size: var(--hwio-type-title-size, 1.125rem); line-height: var(--hwio-type-title-leading, 1.3); letter-spacing: var(--hwio-type-title-tracking, -0.005em); }
84
79
  /* Phones: a heading word wider than its column (a 40 px H1 gets 218-288 px) hyphenates instead
85
80
  of running past the screen, and a token longer than the line (a URL, an ID) breaks. */
86
81
  @media (width < 40rem) {
@@ -143,30 +138,89 @@
143
138
  background: var(--hwio-nav-active, var(--color-primary));
144
139
  }
145
140
 
141
+ /* Kicker: Red caps over a title on the HARDWARIO themes (Brand Guidelines chapter 04 and 05);
142
+ sentence case on er3o. On Ink (neutral panels, and the dark theme's surfaces) it takes the
143
+ light gray, as the guidelines' dark-mode example sets the caps label on an Ink card. */
146
144
  .hwio-kicker,
147
145
  .hwio-eyebrow {
148
146
  color: var(--color-accent);
149
- font-size: 0.875rem;
150
- font-weight: 600;
151
- line-height: 1.4;
147
+ font-size: var(--hwio-type-kicker-size, 0.875rem);
148
+ font-weight: var(--hwio-type-kicker-weight, 600);
149
+ line-height: var(--hwio-type-kicker-leading, 1.4);
150
+ letter-spacing: var(--hwio-type-kicker-tracking, 0);
151
+ text-transform: var(--hwio-type-kicker-case, none);
152
+ }
153
+ .hwio-tone-alt .hwio-kicker,
154
+ .hwio-tone-alt .hwio-eyebrow {
155
+ color: var(--hwio-kicker-on-alt, var(--color-accent));
152
156
  }
153
157
  .hwio-tone-neutral .hwio-kicker,
154
158
  .hwio-tone-neutral .hwio-eyebrow {
155
159
  color: var(--hwio-kicker-on-neutral, var(--color-accent));
156
160
  }
161
+ /* Secondary text: Gray on light grounds, the light gray on dark ones (chapter 03, D35). */
157
162
  .hwio-muted {
158
- color: color-mix(in oklab, var(--color-base-content) 72%, transparent);
163
+ color: var(--hwio-text-secondary, color-mix(in oklab, var(--color-base-content) 72%, transparent));
159
164
  }
160
165
  .hwio-tone-neutral .hwio-muted {
161
- color: color-mix(in oklab, var(--color-neutral-content) 75%, transparent);
166
+ color: var(--hwio-text-secondary-on-neutral, color-mix(in oklab, var(--color-neutral-content) 75%, transparent));
162
167
  }
163
168
  .hwio-lead {
164
- font-size: 1.1875rem;
165
- line-height: 1.55;
166
- color: color-mix(in oklab, var(--color-base-content) 72%, transparent);
169
+ font-size: var(--hwio-type-lead-size, 1.1875rem);
170
+ line-height: var(--hwio-type-lead-leading, 1.55);
171
+ color: var(--hwio-text-secondary, color-mix(in oklab, var(--color-base-content) 72%, transparent));
167
172
  }
168
173
  .hwio-tone-neutral .hwio-lead {
169
- color: color-mix(in oklab, var(--color-neutral-content) 75%, transparent);
174
+ color: var(--hwio-text-secondary-on-neutral, color-mix(in oklab, var(--color-neutral-content) 75%, transparent));
175
+ }
176
+ /* Caption: 12-14 px Regular in Gray (chapter 04). */
177
+ .hwio-caption {
178
+ font-size: var(--hwio-type-caption-size, 0.875rem);
179
+ line-height: var(--hwio-type-caption-leading, 1.4);
180
+ color: var(--hwio-text-secondary, color-mix(in oklab, var(--color-base-content) 72%, transparent));
181
+ }
182
+ .hwio-tone-neutral .hwio-caption {
183
+ color: var(--hwio-text-secondary-on-neutral, color-mix(in oklab, var(--color-neutral-content) 75%, transparent));
184
+ }
185
+ /* Figures (stats): display type, so 1.2 line height at least on the HARDWARIO themes. */
186
+ .hwio-figure {
187
+ font-size: var(--hwio-type-figure-size, 2.25rem);
188
+ font-weight: var(--hwio-type-figure-weight, 700);
189
+ line-height: var(--hwio-type-figure-leading, 1);
190
+ letter-spacing: var(--hwio-type-figure-tracking, -0.03em);
191
+ }
192
+ /* The choice-card title: the H3 role on the HARDWARIO themes, direction A's 28 px on er3o. */
193
+ .hwio-choice-title {
194
+ font-size: var(--hwio-choice-title-size, 1.75rem);
195
+ line-height: var(--hwio-choice-title-leading, 1.15);
196
+ letter-spacing: var(--hwio-choice-title-tracking, -0.02em);
197
+ }
198
+ /* Icons: one colour, Navy (White on dark grounds) on the HARDWARIO themes (chapter 05). */
199
+ .hwio-icon {
200
+ color: var(--hwio-icon, var(--color-accent));
201
+ }
202
+ .hwio-icon-tile {
203
+ background-color: var(--hwio-icon-bg, color-mix(in oklab, var(--color-accent) 8%, transparent));
204
+ color: var(--hwio-icon, var(--color-accent));
205
+ }
206
+ /* Weights the guidelines allow: Regular, Semibold (web H2 and H3) and Bold, no Medium (D22). */
207
+ .hwio-weight-nav { font-weight: var(--hwio-weight-nav, 500); }
208
+ .hwio-weight-label { font-weight: var(--hwio-weight-label, 600); }
209
+ .hwio-weight-footer-title { font-weight: var(--hwio-weight-footer-title, 500); }
210
+ /* Secondary notes (consent texts): Gray at full opacity on the HARDWARIO themes, so a link inside
211
+ keeps its contrast; er3o keeps the text colour at 80 % opacity. */
212
+ .hwio-note {
213
+ color: var(--hwio-text-secondary, currentColor);
214
+ opacity: var(--hwio-note-opacity, 0.8);
215
+ }
216
+ /* An inline text link outside prose: Blue on the HARDWARIO themes (chapter 03 reserves Blue for links). */
217
+ .hwio-link {
218
+ color: var(--hwio-link, var(--color-accent));
219
+ }
220
+ @media (hover: hover) {
221
+ .hwio-link:hover {
222
+ color: var(--hwio-link-hover, color-mix(in oklab, var(--hwio-link, var(--color-accent)) 80%, #000));
223
+ }
170
224
  }
171
225
  .hwio-hairline { border-color: var(--color-base-300); }
172
226
  /* Inverse button for ink/neutral panels (navy on ink has no contrast). */
@@ -186,8 +240,10 @@
186
240
  padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 3);
187
241
  border-radius: 9999px;
188
242
  background: color-mix(in oklab, var(--color-base-content) 6%, transparent);
189
- font-size: var(--text-sm);
190
- font-weight: 500;
243
+ font-size: var(--hwio-type-chip-size, var(--text-sm));
244
+ font-weight: var(--hwio-type-chip-weight, 500);
245
+ letter-spacing: var(--hwio-type-chip-tracking, 0);
246
+ text-transform: var(--hwio-type-chip-case, none);
191
247
  }
192
248
  /* Honeypot: off-screen, never display:none (bots check that), no inline style attribute. */
193
249
  .hwio-hp {
@@ -206,17 +262,49 @@
206
262
  [data-hs-status].hs-status-success { color: var(--color-success); }
207
263
  [data-hs-status].hs-status-error { color: var(--color-error); }
208
264
 
209
- /* Scroll reveal: visible at rest for no-JS, animates in once the runtime marks it. */
210
- .js [data-hwio-reveal]:not([data-hwio-revealed]) {
265
+ /* Scroll reveal, fail-safe (owner, 2026-10-30): content waiting for its reveal hides only once the
266
+ reveal runtime has started (html.hwio-reveal-on, set by @hubpav/hwio-web-runtime/reveal). Without
267
+ JavaScript, before the runtime runs, or if it never loads, everything shows. Hiding is instant;
268
+ revealing fades in and settles once (web layer: ease-out, 400 ms at most, 24 px travel). A page
269
+ can calm it with --hwio-reveal-duration and --hwio-reveal-travel. Stagger siblings with
270
+ data-hwio-reveal-delay="<ms>" (the runtime applies it). */
271
+ html.hwio-reveal-on [data-hwio-reveal]:not([data-hwio-revealed]) {
211
272
  opacity: 0;
212
- transform: translateY(24px);
213
- }
214
- [data-hwio-reveal] {
215
- transition: opacity 0.6s ease-out, transform 0.6s ease-out;
273
+ transform: translateY(var(--hwio-reveal-travel, 24px));
216
274
  }
217
275
  [data-hwio-reveal][data-hwio-revealed] {
218
276
  opacity: 1;
219
277
  transform: none;
278
+ transition:
279
+ opacity var(--hwio-reveal-duration, var(--hwio-motion-reveal, 0.6s)) var(--hwio-motion-easing, ease-out),
280
+ transform var(--hwio-reveal-duration, var(--hwio-motion-reveal, 0.6s)) var(--hwio-motion-easing, ease-out);
281
+ }
282
+
283
+ /* Entrance of a hero once the font gate has shown the page: the children of .hwio-enter fade in and
284
+ settle 12 px, 60 ms apart; .hwio-enter-media follows. Pure CSS that always ends visible, and it
285
+ runs only after the gate has set .fonts-ready (no gate, no animation). */
286
+ @keyframes hwio-enter {
287
+ from { opacity: 0; transform: translateY(12px); }
288
+ to { opacity: 1; transform: none; }
289
+ }
290
+ .fonts-ready .hwio-enter > * {
291
+ animation: hwio-enter var(--hwio-motion-base, 300ms) var(--hwio-motion-easing, ease-out) both;
292
+ }
293
+ .fonts-ready .hwio-enter > :nth-child(2) { animation-delay: 60ms; }
294
+ .fonts-ready .hwio-enter > :nth-child(3) { animation-delay: 120ms; }
295
+ .fonts-ready .hwio-enter > :nth-child(n + 4) { animation-delay: 180ms; }
296
+ .fonts-ready .hwio-enter-media {
297
+ animation: hwio-enter var(--hwio-motion-base, 300ms) var(--hwio-motion-easing, ease-out) 120ms both;
298
+ }
299
+
300
+ /* The arrow of a call to action moves 3 px on hover and keyboard focus (HWioButton's arrow carries
301
+ it; a site marks its own arrow glyph with .hwio-arrow). */
302
+ .hwio-arrow {
303
+ display: inline-block;
304
+ transition: transform var(--hwio-motion-fast, 200ms) var(--hwio-motion-easing, ease-out);
305
+ }
306
+ :is(a, button):is(:hover, :focus-visible) .hwio-arrow {
307
+ transform: translateX(3px);
220
308
  }
221
309
 
222
310
  /* Count-up: reserve the width of the final value so the layout never jumps. */
@@ -255,19 +343,171 @@
255
343
  transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease !important;
256
344
  }
257
345
 
346
+ /* Type roles for elements whose tag is not the role's tag (a card title in an <h3>, a <p> set as H2). */
347
+ .hwio-role-heading {
348
+ font-size: var(--hwio-type-heading-size, 2rem);
349
+ font-weight: var(--hwio-type-heading-weight, 700);
350
+ line-height: var(--hwio-type-heading-leading, 1.1);
351
+ letter-spacing: var(--hwio-type-heading-tracking, -0.025em);
352
+ }
353
+ .hwio-role-subheading {
354
+ font-size: var(--hwio-type-subheading-size, 1.375rem);
355
+ font-weight: var(--hwio-type-subheading-weight, 700);
356
+ line-height: var(--hwio-type-subheading-leading, 1.2);
357
+ letter-spacing: var(--hwio-type-subheading-tracking, -0.01em);
358
+ }
359
+ .hwio-role-title {
360
+ font-size: var(--hwio-type-title-size, 1.125rem);
361
+ font-weight: var(--hwio-type-title-weight, 700);
362
+ line-height: var(--hwio-type-title-leading, 1.3);
363
+ letter-spacing: var(--hwio-type-title-tracking, -0.005em);
364
+ }
365
+
366
+ /* A long H1 (more than about eight words, an opening statement) at 36 / 44 / 48 / 52 px instead of
367
+ the web scale's 40 / 48 / 60 / 72 px, so it reads in three calm lines beside the hero media.
368
+ CHANGE REQUEST CR-7 (owner, 2026-10-30), pending the Brand Guidelines: "a long H1 may go down
369
+ to 48 to 56 px". Opt-in per heading; HWioHeroSplit sets it with titleSize="long". */
370
+ .hwio-h1-long {
371
+ font-size: 2.25rem;
372
+ line-height: var(--hwio-type-display-leading, 1.2);
373
+ letter-spacing: -0.02em;
374
+ }
375
+ @media (width >= 48rem) {
376
+ .hwio-h1-long { font-size: 2.75rem; }
377
+ }
378
+ @media (width >= 64rem) {
379
+ .hwio-h1-long { font-size: 3rem; }
380
+ }
381
+ @media (width >= 80rem) {
382
+ .hwio-h1-long { font-size: 3.25rem; }
383
+ }
384
+
385
+ /* The page's one gradient moment (Brand Guidelines chapter 03; D23, D33, D44): the figure gradient
386
+ fills the type of the key figure (.hwio-key-figure, 40 px and up) or, instead of a key figure,
387
+ of up to two key words of a hero H1 (.hwio-grad). The theme variable is the gradient's left three
388
+ quarters on white (4.5:1) and light red to Sky on the dark ground. Each word carries its own
389
+ gradient. .hwio-grad is CHANGE REQUEST CR-8 (owner, 2026-10-30), pending the guidelines. Themes
390
+ without the gradient (er3o) show plain text: the fill, not `color`, turns transparent, so the
391
+ fallback's currentColor is still the text colour. */
392
+ .hwio-key-figure,
393
+ .hwio-grad {
394
+ background-image: var(--hwio-gradient-figure, linear-gradient(currentColor, currentColor));
395
+ -webkit-background-clip: text;
396
+ background-clip: text;
397
+ -webkit-text-fill-color: transparent;
398
+ -webkit-box-decoration-break: clone;
399
+ box-decoration-break: clone;
400
+ }
401
+ @media (forced-colors: active) {
402
+ .hwio-key-figure,
403
+ .hwio-grad {
404
+ background-image: none;
405
+ -webkit-text-fill-color: CanvasText;
406
+ }
407
+ }
408
+
409
+ /* Linked cards (web layer, States): the title underlines on hover and keyboard focus; no shadow. */
410
+ .hwio-card-link:is(:hover, :focus-visible) .hwio-card-title {
411
+ text-decoration: underline;
412
+ text-underline-offset: 0.18em;
413
+ text-decoration-thickness: 1px;
414
+ }
415
+
416
+ /* White type over a photograph only on the guidelines' scrim (chapter 08): clear over the top
417
+ third, 45 % black at 60 %, 92 % at the bottom. */
418
+ .hwio-scrim {
419
+ background-image: linear-gradient(180deg, rgb(0 0 0 / 0) 33%, rgb(0 0 0 / 0.45) 60%, rgb(0 0 0 / 0.92) 100%);
420
+ }
421
+
422
+ /* Product cut-outs (owner, 2026-10-30; Brand Guidelines chapter 08): one shadow recipe for every
423
+ product, so a row reads as one family: a tight contact shadow plus a soft ambient one; lighter on
424
+ the dark ground. On a common baseline (HWioCutout baseline) a blurred floor ellipse. A cut-out
425
+ sits whole on the Panel ground (.hwio-cutout-ground) or on the page. */
426
+ .hwio-product-shadow {
427
+ filter: drop-shadow(0 2px 2px rgb(37 37 50 / 0.16)) drop-shadow(0 12px 16px rgb(37 37 50 / 0.12));
428
+ }
429
+ [data-theme$='-dark'] .hwio-product-shadow {
430
+ filter: drop-shadow(0 6px 12px rgb(0 0 0 / 0.35));
431
+ }
432
+ .hwio-product-floor {
433
+ position: absolute;
434
+ left: 20%;
435
+ right: 20%;
436
+ height: 7%;
437
+ border-radius: 50%;
438
+ background: radial-gradient(closest-side, rgb(37 37 50 / 0.18), rgb(37 37 50 / 0));
439
+ pointer-events: none;
440
+ }
441
+ [data-theme$='-dark'] .hwio-product-floor {
442
+ display: none;
443
+ }
444
+ .hwio-cutout-ground {
445
+ background-color: var(--color-base-200);
446
+ }
447
+ /* A drawn isometric room behind a cut-out (HWioCutout backdrop): it fills the frame (the kit's SVG
448
+ carries a 1 pt margin, hidden by the slight scale); the dark-theme drawing on the dark ground. */
449
+ .hwio-backdrop {
450
+ position: absolute;
451
+ inset: 0;
452
+ width: 100%;
453
+ height: 100%;
454
+ object-fit: cover;
455
+ transform: scale(1.012);
456
+ pointer-events: none;
457
+ }
458
+ .hwio-backdrop-dark { display: none; }
459
+ [data-theme$='-dark'] .hwio-backdrop-light { display: none; }
460
+ [data-theme$='-dark'] .hwio-backdrop-dark { display: block; }
461
+
462
+ /* Menu panels (HWioMegaLinks) and the drawer's second lines (owner, 2026-10-30): a caps group title
463
+ in Gray, link labels that never wrap, descriptions small (13 px), Regular, Gray and balanced. */
464
+ .hwio-mega-title {
465
+ color: var(--hwio-text-secondary, color-mix(in oklab, var(--color-base-content) 72%, transparent));
466
+ font-size: var(--hwio-type-kicker-size, 0.875rem);
467
+ font-weight: var(--hwio-type-kicker-weight, 600);
468
+ line-height: var(--hwio-type-kicker-leading, 1.4);
469
+ letter-spacing: var(--hwio-type-kicker-tracking, 0);
470
+ text-transform: var(--hwio-type-kicker-case, none);
471
+ }
472
+ .hwio-mega-label {
473
+ display: inline-flex;
474
+ align-items: center;
475
+ gap: 0.375rem;
476
+ white-space: nowrap;
477
+ color: var(--color-base-content);
478
+ font-size: 0.9375rem;
479
+ font-weight: var(--hwio-weight-label, 600);
480
+ line-height: 1.35;
481
+ transition: color var(--hwio-motion-fast, 200ms) var(--hwio-motion-easing, ease-out);
482
+ }
483
+ a:is(:hover, :focus-visible) .hwio-mega-label {
484
+ color: var(--hwio-nav-hover, var(--color-primary));
485
+ }
486
+ .hwio-menu-desc {
487
+ color: var(--hwio-text-secondary, color-mix(in oklab, var(--color-base-content) 72%, transparent));
488
+ font-size: 0.8125rem;
489
+ font-weight: 400;
490
+ line-height: 1.45;
491
+ text-wrap: balance;
492
+ }
493
+
258
494
  @media (prefers-reduced-motion: reduce) {
259
495
  [data-hwio-reveal] { opacity: 1 !important; transform: none !important; transition: none !important; }
260
496
  .hwio-marquee-track { animation: none; transform: none; will-change: auto; }
261
497
  .hwio-marquee-track > [aria-hidden='true'] { display: none; }
498
+ .fonts-ready .hwio-enter > *,
499
+ .fonts-ready .hwio-enter-media { animation: none; }
500
+ .hwio-arrow,
501
+ :is(a, button):is(:hover, :focus-visible) .hwio-arrow { transition: none; transform: none; }
262
502
  }
263
503
  }
264
504
 
265
505
  /* Brand overrides of DaisyUI defaults. Unlayered on purpose: DaisyUI lives in the utilities layer
266
- and would otherwise win. Direction A: 15 px/600 sentence-case buttons, 48 px tall, 8 px radius,
267
- 28 px padding on large buttons at the same height; card padding 32 px. */
506
+ and would otherwise win. 15 px sentence-case buttons (Bold on the HARDWARIO themes, 600 on er3o),
507
+ 48 px tall, 8 px radius, 28 px padding on large buttons at the same height; card padding 32 px. */
268
508
  .btn {
269
- --fontsize: 0.9375rem;
270
- font-weight: 600;
509
+ --fontsize: var(--hwio-type-button-size, 0.9375rem);
510
+ font-weight: var(--hwio-type-button-weight, 600);
271
511
  padding-inline: 1.5rem;
272
512
  }
273
513
  .btn-sm { --fontsize: 0.875rem; padding-inline: 1rem; }
@@ -277,6 +517,12 @@
277
517
  /* DaisyUI sets .label to 60 % of the text colour, which fails AA at 14 px (4.1:1 on white). R5:
278
518
  darken muted text only. 78 % keeps the label visibly secondary and clears 4.5:1 on every theme. */
279
519
  .label { color: color-mix(in oklab, currentColor 78%, transparent); }
520
+ /* Table heads: DaisyUI sets 60 % of the text colour (4.1:1 on white at 14 px) in Semibold; on the
521
+ HARDWARIO themes secondary text (Gray, 5.3:1) in Bold (D22), unchanged on er3o. */
522
+ .table :where(thead, tfoot) {
523
+ color: var(--hwio-text-secondary, color-mix(in oklab, var(--color-base-content) 60%, transparent));
524
+ font-weight: var(--hwio-weight-label, 600);
525
+ }
280
526
  /* Same for inactive tabs (DaisyUI: 50 %, 3.1:1 on white at 14 px). */
281
527
  .tab:not(.tab-active, :hover, :focus-visible) { color: color-mix(in oklab, currentColor 78%, transparent); }
282
528
  /* Text links: themes whose primary is too light for text on the page ground (er3o teal) set
@@ -309,6 +555,35 @@
309
555
  .hwio-nav-tight > .hwio-nav-current::after {
310
556
  inset-inline: 0.5rem;
311
557
  }
558
+ header:has(.hwio-nav-tight) .hwio-utility > summary.btn {
559
+ --fontsize: 14px;
560
+ font-size: 14px;
561
+ }
562
+ }
563
+ /* Header utilities (owner, 2026-10-30): HWioHeader's theme toggle and language dropdown read as quiet
564
+ utilities beside the nav, not as buttons: the nav's weight (Regular on the HARDWARIO themes), size
565
+ and 80 % ink, the nav's hover colour, the toggle's icon at 18 px (HWioIcon's 1.5 stroke), the
566
+ chevron as faint as the nav chevrons. Unlayered, because DaisyUI's .btn sits in the utilities layer. */
567
+ .hwio-utility.btn,
568
+ .hwio-utility > summary.btn {
569
+ --fontsize: 0.9375rem;
570
+ font-size: 0.9375rem;
571
+ font-weight: var(--hwio-weight-nav, 500);
572
+ letter-spacing: 0;
573
+ color: color-mix(in oklab, var(--color-base-content) 80%, transparent);
574
+ border-color: transparent;
575
+ box-shadow: none;
576
+ }
577
+ .hwio-utility.btn:hover,
578
+ .hwio-utility > summary.btn:hover {
579
+ color: var(--hwio-nav-hover, var(--color-accent));
580
+ }
581
+ .hwio-utility.btn > svg {
582
+ width: 1.125rem;
583
+ height: 1.125rem;
584
+ }
585
+ .hwio-utility > summary.btn > svg:last-child {
586
+ opacity: 0.5;
312
587
  }
313
588
  /* DaisyUI 5.7.30+ also gives pressed buttons (aria-pressed / aria-checked) a filled ground. A ghost
314
589
  toggle shows its state in its own content (HWioThemeToggle swaps moon and sun and sets
@@ -318,3 +593,16 @@
318
593
  --btn-bg: #0000;
319
594
  --btn-border: #0000;
320
595
  }
596
+ /* Forms on the brand type scale (owner, 2026-11-01; website-admin DESIGN-SYSTEM.md, web layer type table: "Form
597
+ label 15 Bold", "Form field text 16 Regular"). DaisyUI's 12 px Semibold field labels and 14 px fields sat below
598
+ the body text, and iPhones zoom into a focused field whose text is under 16 px. Unlayered, so it wins over
599
+ DaisyUI's component layer. */
600
+ .fieldset-legend {
601
+ font-size: 0.9375rem;
602
+ font-weight: var(--hwio-weight-label, 700);
603
+ }
604
+ .input,
605
+ .select,
606
+ .textarea {
607
+ font-size: 1rem;
608
+ }