@bytebrand/fe-ui-core-autobahn 1.0.118 → 1.0.119

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.
package/cards/CarCard.tsx CHANGED
@@ -20,7 +20,7 @@
20
20
  import React, { useState, useEffect, useRef } from 'react';
21
21
  import { TranslationFunction } from '../types';
22
22
 
23
- import { CardCarousel, IconButton, SaveChip, Button } from '../ui';
23
+ import { CardCarousel, SaveChip, Button } from '../ui';
24
24
  import {
25
25
  SpecLine,
26
26
  EcoLine,
@@ -174,10 +174,30 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
174
174
  {/* max two overlays: heart + ONE deal signal — rating moved next to the price */}
175
175
  <div
176
176
  className="cc-fav"
177
- style={{ position: 'absolute', top: 6, right: 6, zIndex: 6 }}
177
+ style={{ position: 'absolute', top: 14, right: 9, zIndex: 6 }}
178
178
  onClick={(e) => e.stopPropagation()}
179
179
  >
180
- <IconButton name="heart" filled active={isFavorite} onClick={onToggleFavorite} label={t('autobahn:card.action.save')} />
180
+ {/* Custom heart (not lucide's default 'heart') — scoped here via a hand-rolled
181
+ button+svg instead of IconButton/Icon's name-based registry lookup, so this
182
+ doesn't change the "heart" icon anywhere else it's used (e.g. CarRow). Same
183
+ active/inactive color logic IconButton+Icon had: active fills solid
184
+ var(--md-primary), inactive stays an outline (previously 'currentColor',
185
+ now this Figma-exact icon's own #08162A stroke). */}
186
+ <button
187
+ className="icon-btn icon-btn--filled"
188
+ onClick={onToggleFavorite}
189
+ aria-label={t('autobahn:card.action.save')}
190
+ >
191
+ <svg width="22" height="22" viewBox="0 0 22 22" fill={isFavorite ? 'var(--md-primary)' : 'none'} xmlns="http://www.w3.org/2000/svg">
192
+ <path
193
+ d="M11 18.7915C11 18.7915 4.125 14.8498 4.125 9.34981C4.12463 8.54932 4.37379 7.76862 4.83781 7.11634C5.30183 6.46405 5.95763 5.97262 6.71396 5.71043C7.47029 5.44823 8.28955 5.42831 9.05773 5.65343C9.82591 5.87855 10.5048 6.33752 11 6.96648C11.4952 6.33752 12.1741 5.87855 12.9423 5.65343C13.7105 5.42831 14.5297 5.44823 15.286 5.71043C16.0424 5.97262 16.6982 6.46405 17.1622 7.11634C17.6262 7.76862 17.8754 8.54932 17.875 9.34981C17.875 14.8498 11 18.7915 11 18.7915Z"
194
+ stroke={isFavorite ? 'var(--md-primary)' : '#08162A'}
195
+ strokeWidth={1.8}
196
+ strokeLinecap="round"
197
+ strokeLinejoin="round"
198
+ />
199
+ </svg>
200
+ </button>
181
201
  </div>
182
202
  {/* top-left signal stack (heart is top-right): NEU badge + ONE deal signal + the
183
203
  "⚡ Angebot" badge (own line, Figma) below it. DropChip takes precedence over
@@ -187,7 +207,7 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
187
207
  className="cc-badges"
188
208
  style={{
189
209
  position: 'absolute',
190
- top: 10,
210
+ top: 16,
191
211
  left: 10,
192
212
  zIndex: 3,
193
213
  display: 'flex',
@@ -213,7 +233,7 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
213
233
  background: 'rgb(204, 222, 255)',
214
234
  border: '1px solid rgba(0, 92, 255, 0.05)',
215
235
  borderRadius: 15,
216
- fontFamily: 'Arial',
236
+ fontFamily: 'Arial, Arimo, sans-serif',
217
237
  fontWeight: 700,
218
238
  fontSize: 13,
219
239
  lineHeight: '15px',
@@ -240,7 +260,11 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
240
260
  <GalleryActivityPill viewsCount={car.viewsCount} favoritesCount={car.favoritesCount} />
241
261
  </div>
242
262
  </div>
243
- <div style={{ padding: '8px 18px', display: 'flex', flexDirection: 'column', gap: 0, flex: 1 }}>
263
+ {/* padding-top: 0 (was 8px) + marginTop: -5 — not just 0, the ask was 0 AND 5px higher
264
+ still than that zero-padding baseline, so a negative margin pulls the whole block up
265
+ past where padding-top:0 alone would leave it. Bottom/side padding (8px/18px)
266
+ untouched. */}
267
+ <div style={{ padding: '0 18px 8px', marginTop: -3, display: 'flex', flexDirection: 'column', gap: 0, flex: 1 }}>
244
268
  <div>
245
269
  <div className="md-title" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
246
270
  {car.make} {car.model}
@@ -250,7 +274,7 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
250
274
  </div>
251
275
  </div>
252
276
  <SpecLine items={cardSpec(car)} oneLine />
253
- <div className="cc-price" style={{ marginTop: 4, borderTop: '1px solid var(--md-outline-variant)' }}>
277
+ <div className="cc-price" style={{ marginTop: 0, borderTop: '1px solid var(--md-outline-variant)' }}>
254
278
  <PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} />
255
279
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginTop: 10 }}>
256
280
  <PriceHero car={car} mode={mode} t={t} alwaysShowUnit />
@@ -283,7 +307,7 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
283
307
  style={{
284
308
  marginTop: 'auto',
285
309
  borderTop: 'none',
286
- fontFamily: 'Arial, Helvetica, sans-serif',
310
+ fontFamily: 'Arial, Helvetica, Arimo, sans-serif',
287
311
  fontStyle: 'normal',
288
312
  fontWeight: 400,
289
313
  fontSize: 11,
@@ -56,6 +56,18 @@ function stripParen(s: string): string {
56
56
  return s.replace(/\s*\(.*\)/, '');
57
57
  }
58
58
 
59
+ /**
60
+ * Opposite of stripParen, for car.power specifically: pull the PS value OUT of the trailing
61
+ * parenthetical instead of discarding it, e.g. '145 kW (197 PS)' → '197 PS' — the card wants
62
+ * PS only, no kW at all. Regex requires the paren to actually contain "N PS" (not just any
63
+ * parenthetical) and falls back to the plain stripParen(kW) result when it doesn't, so a power
64
+ * string without a PS conversion still renders something instead of going blank.
65
+ */
66
+ function extractPS(s: string): string {
67
+ const m = s.match(/\((\d+\s*PS)\)\s*$/i);
68
+ return m ? m[1].replace(/\s+/, ' ') : stripParen(s);
69
+ }
70
+
59
71
  // Compact spec for cards: short fuel/gear labels so all five values fit ONE line
60
72
  // without clipping.
61
73
  const FUEL_SHORT: Record<string, string> = {
@@ -75,7 +87,7 @@ export function cardSpec(car: AutobahnCar): string[] {
75
87
  return [
76
88
  fmtKm(car.km),
77
89
  car.reg,
78
- stripParen(car.power),
90
+ extractPS(car.power),
79
91
  FUEL_SHORT[fuel] || fuel,
80
92
  GEAR_SHORT[car.gear] || car.gear,
81
93
  ];
@@ -799,7 +811,7 @@ export const CardCtaRow: React.FunctionComponent<CardCtaRowProps> = ({ car, mode
799
811
  background: '#005cff',
800
812
  borderRadius: 16,
801
813
  padding: '0 8px',
802
- fontFamily: 'Arial',
814
+ fontFamily: 'Arial, Arimo, sans-serif',
803
815
  fontStyle: 'normal',
804
816
  fontWeight: 700,
805
817
  fontSize: 13,
@@ -877,7 +889,7 @@ export const CardCtaRow: React.FunctionComponent<CardCtaRowProps> = ({ car, mode
877
889
  border: '1.6px solid #5b6b7f',
878
890
  borderRadius: 16,
879
891
  padding: '0 6px',
880
- fontFamily: 'Arial',
892
+ fontFamily: 'Arial, Arimo, sans-serif',
881
893
  fontStyle: 'normal',
882
894
  fontWeight: 700,
883
895
  fontSize: 13,
@@ -6,7 +6,7 @@
6
6
  margin: 0
7
7
 
8
8
  background-color: $primary
9
- font-family: 'Arial', sans-serif
9
+ font-family: 'Arial', 'Arimo', sans-serif
10
10
 
11
11
  min-height: 28px
12
12
  min-width: 140px
@@ -4,7 +4,12 @@
4
4
  position: relative
5
5
  display: inline-block
6
6
  vertical-align: middle
7
- outline: none !important
7
+ // Was `outline: none !important` — the !important beat .container:focus below regardless
8
+ // of it being more specific (pseudo-class) or coming later, permanently killing the
9
+ // custom focus outline that rule was clearly written to show. This class only sets the
10
+ // resting-state default (no !important needed — nothing else here competes for it), so
11
+ // :focus's own outline declaration can win normally on focus, as intended.
12
+ outline: none
8
13
 
9
14
  .wrapInput
10
15
  display flex;
@@ -115,7 +115,17 @@ class Checkbox extends React.Component<ICheckboxProps, {}> {
115
115
  );
116
116
 
117
117
  return (
118
- <div className={containerClasses} tabIndex={1} onFocus={this.props.onFocus} onBlur={this.props.onBlur} onKeyPress={this.onKeyPress}>
118
+ // tabIndex: was 1 (a positive value) — a well-known anti-pattern (WCAG 2.4.3):
119
+ // positive tabindex elements are visited in their own ascending-order pass BEFORE
120
+ // every tabindex="0"/naturally-focusable element on the page, regardless of where
121
+ // they sit in the DOM. In BaseModal's new focus trap (which walks the panel's
122
+ // focusable elements in DOM order to decide where Tab should wrap) that mismatch
123
+ // made this checkbox unreachable by Tab entirely inside a trapped modal — verified
124
+ // live: 0 hits in 20 Tab presses cycling the RegistrationForm consent checkbox.
125
+ // 0 makes it a normal stop in DOM-order tab flow, matching the trap's own model and
126
+ // putting it where a sighted user would visually expect it (after the fields above,
127
+ // before the CTA below) instead of unpredictably early.
128
+ <div className={containerClasses} tabIndex={0} onFocus={this.props.onFocus} onBlur={this.props.onBlur} onKeyPress={this.onKeyPress}>
119
129
  <label className={labelClasses}>
120
130
  <div className={wrapInputClasses}>
121
131
  {/* Visual representation of the checkbox */}
@@ -11,7 +11,8 @@
11
11
  display: block
12
12
  width: 100%
13
13
  z-index: 1
14
- font-family: "Arial Standard", Arial
14
+ // "Arial Standard" isn't a real installed font name anywhere — dead fallback, dropped.
15
+ font-family: Arial, Arimo, sans-serif
15
16
  font-weight: 400;
16
17
  font-style: normal
17
18
  font-size: 10px
@@ -78,7 +78,7 @@
78
78
  align-items: center !important
79
79
  justify-content: center !important
80
80
  text-align: center !important
81
- font-family: 'Arial' !important
81
+ font-family: 'Arial', 'Arimo', sans-serif !important
82
82
  font-style: normal !important
83
83
  font-weight: 400 !important
84
84
  font-size: 14px !important
@@ -17,7 +17,7 @@
17
17
  margin-top: 20px
18
18
 
19
19
  +media-tablet-down()
20
- font-family: 'Arial';
20
+ font-family: 'Arial', 'Arimo', sans-serif;
21
21
  font-weight: 700;
22
22
  font-size: 16px;
23
23
  line-height: 32px;
@@ -27,7 +27,7 @@
27
27
 
28
28
  .modalContent
29
29
  margin-top: 3px
30
- font-family: 'Arial'
30
+ font-family: 'Arial', 'Arimo', sans-serif
31
31
  font-weight: 400
32
32
  font-size: 14px;
33
33
  line-height: 16px;
@@ -35,7 +35,7 @@
35
35
  letter-spacing: 0.15px;
36
36
  color: rgba(76, 78, 100, 0.87);
37
37
  +media-tablet-down()
38
- font-family: 'Arial';
38
+ font-family: 'Arial', 'Arimo', sans-serif;
39
39
  font-weight: 400;
40
40
  font-size: 14px;
41
41
  line-height: 16px;
@@ -77,7 +77,7 @@
77
77
  background: #005CFF !important
78
78
  box-shadow: 0px 4px 8px -4px rgba(76, 78, 100, 0.42) !important
79
79
  border-radius: 16px !important
80
- font-family: 'Arial' !important
80
+ font-family: 'Arial', 'Arimo', sans-serif !important
81
81
  font-weight: 500 !important
82
82
  font-size: 14px !important
83
83
  line-height: 24px !important
@@ -97,7 +97,7 @@
97
97
  align-items: center !important
98
98
  justify-content: center !important
99
99
  text-align: center !important
100
- font-family: 'Arial' !important
100
+ font-family: 'Arial', 'Arimo', sans-serif !important
101
101
  font-style: normal !important
102
102
  font-weight: 400 !important
103
103
  font-size: 14px !important
@@ -115,7 +115,7 @@
115
115
  align-items: center !important
116
116
  justify-content: center !important
117
117
  text-align: center !important
118
- font-family: 'Arial' !important
118
+ font-family: 'Arial', 'Arimo', sans-serif !important
119
119
  font-style: normal !important
120
120
  font-weight: 400 !important
121
121
  font-size: 14px !important
@@ -125,7 +125,7 @@
125
125
  .outlinedBtnLeft
126
126
  display: flex;
127
127
  justify-content: flex-end;
128
- font-family: 'Arial';
128
+ font-family: 'Arial', 'Arimo', sans-serif;
129
129
  font-style: normal;
130
130
  font-weight: 400;
131
131
  color: #005CCB;
@@ -45,6 +45,9 @@ const MdField: React.FunctionComponent<MdFieldProps> = ({
45
45
  const inputType = isPassword && reveal ? 'text' : (type || 'text');
46
46
  const hasError = !!error;
47
47
  const generatedId = React.useId();
48
+ // Stable id linking the input to its (visually hidden, see .mfield-error-text sr-only
49
+ // CSS) error text — lets aria-describedby work even when the text itself is invisible.
50
+ const errorId = `${generatedId}-error`;
48
51
  // floating label (Material pattern): while empty and unfocused, the label text is
49
52
  // shown as the input's own native placeholder (styled via .mfield--floating
50
53
  // input::placeholder) and the <label> itself is not rendered at all; once focused or
@@ -53,6 +56,14 @@ const MdField: React.FunctionComponent<MdFieldProps> = ({
53
56
  const hasValue = value != null && value !== '';
54
57
  const resting = !!floatingLabel && !focused && !hasValue;
55
58
  const effectivePlaceholder = floatingLabel ? (resting ? (label || placeholder || '') : '') : (placeholder || '');
59
+ // The real <label> is skipped entirely while resting (see above), leaving the input with
60
+ // no programmatic name at all — `placeholder` alone is not a reliable accessible-name
61
+ // fallback across browser/AT combinations, and WCAG explicitly advises against relying on
62
+ // it. aria-label steps in for exactly that gap; once floated (focused or has a value) the
63
+ // real <label htmlFor> takes over as the name source and this reverts to undefined rather
64
+ // than sitting there as a redundant duplicate (aria-label always outranks a wired <label>
65
+ // in the accessible-name computation, so leaving it set wouldn't be wrong, just noise).
66
+ const ariaLabel = resting ? (label || placeholder || undefined) : undefined;
56
67
 
57
68
  const handleChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
58
69
  onChange(event.currentTarget.value, event.currentTarget.name || name, event);
@@ -86,6 +97,9 @@ const MdField: React.FunctionComponent<MdFieldProps> = ({
86
97
  maxLength={maxLength}
87
98
  placeholder={effectivePlaceholder}
88
99
  autoComplete={autoComplete}
100
+ aria-label={ariaLabel}
101
+ aria-invalid={hasError ? true : undefined}
102
+ aria-describedby={hasError ? errorId : undefined}
89
103
  style={{ width: '100%', resize: 'vertical' }}
90
104
  />
91
105
  ) : (
@@ -101,6 +115,9 @@ const MdField: React.FunctionComponent<MdFieldProps> = ({
101
115
  maxLength={maxLength}
102
116
  placeholder={effectivePlaceholder}
103
117
  autoComplete={autoComplete}
118
+ aria-label={ariaLabel}
119
+ aria-invalid={hasError ? true : undefined}
120
+ aria-describedby={hasError ? errorId : undefined}
104
121
  style={{ width: '100%' }}
105
122
  />
106
123
  )}
@@ -116,7 +133,13 @@ const MdField: React.FunctionComponent<MdFieldProps> = ({
116
133
  </button>
117
134
  ) : null}
118
135
  </div>
119
- {hasError ? <span className='mfield-error-text'>{error}</span> : null}
136
+ {hasError ? (
137
+ // role="alert" announces this to screen readers the moment it enters the DOM (e.g.
138
+ // right after a failed Submit) — no need to wait for focus to reach this field.
139
+ // Visually hidden or not is entirely a CSS concern (.mfield-error-text) — this
140
+ // element and its id/role/text content exist unconditionally on error either way.
141
+ <span id={errorId} className='mfield-error-text' role='alert'>{error}</span>
142
+ ) : null}
120
143
  </div>
121
144
  );
122
145
  };
@@ -71,6 +71,9 @@ const MdSelect: React.FunctionComponent<MdSelectProps> = ({
71
71
  disabled: !!item.isDisabled
72
72
  }));
73
73
  const hasError = !!error;
74
+ // Stable id linking the trigger to its (visually hidden, see .mfield-error-text sr-only
75
+ // CSS) error text — lets aria-describedby work even when the text itself is invisible.
76
+ const errorId = React.useId() + '-error';
74
77
 
75
78
  const handleChange = (picked: string) => {
76
79
  const match = (items || []).find(item => normalize(item.value) === picked);
@@ -94,8 +97,14 @@ const MdSelect: React.FunctionComponent<MdSelectProps> = ({
94
97
  placeholder={placeholder || (label ? label : undefined)}
95
98
  disabled={disabled}
96
99
  ariaLabel={label}
100
+ ariaInvalid={hasError}
101
+ ariaDescribedBy={hasError ? errorId : undefined}
97
102
  />
98
- {hasError && typeof error === 'string' ? <span className='mfield-error-text'>{error}</span> : null}
103
+ {hasError && typeof error === 'string' ? (
104
+ // role="alert" announces this to screen readers the moment it enters the DOM (e.g.
105
+ // right after a failed Submit) — no need to wait for focus to reach this field.
106
+ <span id={errorId} className='mfield-error-text' role='alert'>{error}</span>
107
+ ) : null}
99
108
  </div>
100
109
  );
101
110
  };
@@ -5,7 +5,12 @@
5
5
  box-sizing: border-box
6
6
  padding: 12px 18px
7
7
  background-color: white
8
- font-family: 'Arial Fett', 'Arial Standard', 'Arial'
8
+ // was 'Arial Fett', 'Arial Standard', 'Arial' — those first two aren't real installed font
9
+ // names anywhere ("Fett" = German for bold), a dead attempt to force bold via font-family
10
+ // instead of font-weight. The real @font-face (theme/autobahn.css) now gives 'Arial' a proper
11
+ // bold face at weight 700.
12
+ font-family: 'Arial', 'Arimo', sans-serif
13
+ font-weight: 700
9
14
  border-radius: 10px
10
15
  border: 1px solid $grey-e
11
16
  width: 100%
@@ -33,7 +33,7 @@
33
33
  padding: 3px 6px
34
34
  border-radius: 50%
35
35
  background-color: $primary
36
- font-family: 'Arial-BoldMT', 'Arial Bold', 'Arial'
36
+ font-family: 'Arial', 'Arimo', sans-serif
37
37
  font-weight: 700
38
38
  font-size: 10px
39
39
  color: white
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.118",
3
+ "version": "1.0.119",
4
4
  "description": "auto.de md3 (Autobahn) design system — UI primitives, cards, chrome, VDP modules, theme CSS and locales. Ships raw TypeScript source (like fe-ui-core before it); each consumer's bundler compiles it.",
5
5
  "main": "index.ts",
6
6
  "license": "UNLICENSED",
@@ -8,21 +8,63 @@
8
8
  subtree receive Autobahn tokens + utility classes.
9
9
 
10
10
  Load order:
11
- 1. Google Fonts (Schibsted Grotesk + Hanken Grotesk + Inter). Per the W1
12
- plan the CDN @import is kept for the pilot; self-hosting woff2 is a
13
- later ticket. CSS requires @import to precede all other rules, so it
14
- stays first. Inter (400/500/700) is scoped to VdpViewer's mobile
11
+ 1. Google Fonts (Schibsted Grotesk + Hanken Grotesk + Inter + Arimo). Per
12
+ the W1 plan the CDN @import is kept for the pilot; self-hosting woff2
13
+ is a later ticket. CSS requires @import to precede all other rules, so
14
+ it stays first. Inter (400/500/700) is scoped to VdpViewer's mobile
15
15
  gallery — that layout is a pixel-match of a Figma frame authored in
16
16
  Inter, not the Hanken Grotesk used by the rest of the design system.
17
+ Arimo (400/500/700) is NOT part of the Autobahn design language at
18
+ all — it's loaded purely as a REAL, always-available webfont fallback
19
+ for the many legacy (non-.md3) components across fe-main/fe-landing
20
+ that hardcode font-family: 'Arial' (see the @font-face block below).
17
21
  2. colors_and_type.css — brand tokens (.md3 { --autode-* / --primary ... }).
18
22
  3. md3.css — MD3 utility classes + responsive layer (.mbtn/.mcard/.chip/
19
23
  .segmented/.mfield/.num/.md-* + 1024/980/900/820/600px breakpoints).
20
24
 
21
25
  ADDITIVE ONLY. Do not import this from a non-`.md3` context expecting it to
22
26
  restyle existing components — it intentionally does nothing outside `.md3`.
27
+ The @font-face block below is the one exception: @font-face is never scoped
28
+ by a selector, so it applies to the whole page (.md3 or not) the moment
29
+ this stylesheet loads — both fe-main (App.global.styl) and fe-landing
30
+ (pages/_app.tsx) load it globally already.
23
31
  ========================================================================== */
24
32
 
25
- @import url('https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@500;600;700;800&family=Hanken+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;700&display=swap');
33
+ @import url('https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@500;600;700;800&family=Hanken+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;700&family=Arimo:wght@400;500;700&display=swap');
26
34
 
27
35
  @import './colors_and_type.css';
28
36
  @import './md3.css';
37
+
38
+ /* Arial weight fix: dozens of legacy (non-.md3) components across fe-main/fe-landing hardcode
39
+ font-family: 'Arial' + an explicit font-weight (400/700), relying entirely on whatever the
40
+ visitor's OS happens to have installed under that exact name. Real Arial (Windows/macOS) ships
41
+ true separate Regular/Bold font files, but without an explicit @font-face some browsers
42
+ inconsistently pick the nearest installed match or synthesize ("fake") bold instead of using
43
+ the real bold face — that's the "wrong font-weight" symptom. This does NOT redistribute Arial
44
+ (proprietary/Microsoft-licensed, can't legally be self-hosted) — local() only queries fonts
45
+ ALREADY installed on the device by name, same matching the browser does implicitly anyway,
46
+ just made explicit and given metric-compatible open fallbacks (Liberation Sans on many Linux
47
+ distros, Arimo — Android's own built-in substitute for "Arial") so real/near-real weight faces
48
+ are used more often instead of synthesized ones. Where NONE of these are installed (e.g. iOS,
49
+ which bundles none of the three), this still falls through to each declaration's own trailing
50
+ `sans-serif` same as before — it cannot add coverage that isn't on the device. */
51
+ @font-face {
52
+ font-family: 'Arial';
53
+ font-weight: 400;
54
+ font-style: normal;
55
+ src: local('Arial'), local('ArialMT'), local('Liberation Sans'), local('Arimo');
56
+ }
57
+ @font-face {
58
+ font-family: 'Arial';
59
+ font-weight: 500;
60
+ font-style: normal;
61
+ /* Arial itself has no true Medium cut — Liberation Sans/Arimo do, so this at least gets a
62
+ real medium face on the platforms that have them instead of always synthesizing one. */
63
+ src: local('Liberation Sans Medium'), local('Arimo Medium'), local('Arial'), local('ArialMT');
64
+ }
65
+ @font-face {
66
+ font-family: 'Arial';
67
+ font-weight: 700;
68
+ font-style: normal;
69
+ src: local('Arial Bold'), local('Arial-BoldMT'), local('Liberation Sans Bold'), local('Arimo Bold');
70
+ }