@atelic-action/ui 2.1.0 → 2.2.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.
package/README.md CHANGED
@@ -22,6 +22,8 @@ Every export, grouped by the entry it is imported from. The gallery column links
22
22
  | `Checklist` | `@atelic-action/ui/components` | A short list of checks, each passed or failed | [#checklist](https://ui.atelic.me/#checklist) |
23
23
  | `IconList` | `@atelic-action/ui/components` | A list whose bullets are icons, one for the list or one per item, each in a tone | [#icon-list](https://ui.atelic.me/#icon-list) |
24
24
  | `DataTable` | `@atelic-action/ui/components` | A table whose first column names each row, sortable, stacking on a phone | [#data-table](https://ui.atelic.me/#data-table) |
25
+ | `ReportCard` | `@atelic-action/ui/components` | A name, a grade, and a line per row, in labeled groups, each row a link to its section | [#report-card](https://ui.atelic.me/#report-card) |
26
+ | `Rank` | `@atelic-action/ui/components` | A figure with its badge in a tone, for a table cell: a rank, a place in an answer, or a dash | [#rank](https://ui.atelic.me/#rank) |
25
27
  | `StatRow` | `@atelic-action/ui/components` | A row of large figures with periods, deltas, and trends | [#stat-row](https://ui.atelic.me/#stat-row) |
26
28
  | `Callout` | `@atelic-action/ui/components` | One finding: an icon in its tone, the title, the explainer | [#callout](https://ui.atelic.me/#callout) |
27
29
  | `Disclosure` | `@atelic-action/ui/components` | A section that folds: controlled, an app's card; uncontrolled, a native details fold that prints open | [#disclosure](https://ui.atelic.me/#disclosure) |
@@ -353,7 +355,7 @@ export function ArtifactShell(props: Omit<ArtifactShellProps, "currentPath">) {
353
355
  }
354
356
  ```
355
357
 
356
- The pieces import no stylesheet of their own. A site imports `@atelic-action/ui/styles/artifact.css` in its base sheet after `gate.css`, then `@atelic-action/artifact/styles/documents.css`, and then its `theme.css`, so every page carries the rules whichever route it is. This sheet holds the pieces alone (the grade chip, the sign off, the shot, and the shell's logo rule) and sets no `@page`; both `@page` rules, the monthly page's and then the writeup's, moved with the documents into their sheet, where their order stays load bearing.
358
+ The pieces import no stylesheet of their own. A site imports `@atelic-action/ui/styles/artifact.css` in its base sheet after `gate.css`, then `@atelic-action/artifact/styles/documents.css`, and then its `theme.css`, so every page carries the rules whichever route it is. This sheet holds the pieces alone (the sign off, the shot, and the shell's logo rule; the grade chip's rules sit in `components.css`, since `ReportCard` wears it) and sets no `@page`; both `@page` rules, the monthly page's and then the writeup's, moved with the documents into their sheet, where their order stays load bearing.
357
359
 
358
360
  ### The Artifact Vite Config
359
361
 
@@ -696,6 +698,8 @@ The blocks a page composes, each styled in `styles/components.css` inside the `a
696
698
  |---|---|---|
697
699
  | `Checklist` | A short list of checks, each passed or failed | Two columns when there is room, one on a phone. `passLabel` and `failLabel` name the icons for a screen reader |
698
700
  | `IconList` | A list whose bullets are icons | `icon` and `tone` on the list set every bullet; an item's own override them. The bullet is decoration, so a screen reader hears only the items. A dot when no icon is set; the text's color when no tone is |
701
+ | `ReportCard` | A name, a grade, and one line on it per row, in labeled groups, each row's parts centered on it | A row with `href` is a link from edge to edge with a chevron at its end; a link inside `line` still goes where it points. `before` sets the earlier grade faded beside the grade; no `grade` draws a dash read as not graded. `legend` explains the letters beside the `title`, or under the card with `legendAt="foot"`; `labels` sets the screen reader's words. The grade is `GradeChip` |
702
+ | `Rank` | A figure with its badge, for a table cell | `figure` is the rank, the place, or a dash for none; `tone` colors the badge and `meaning` names it for a screen reader; `href` links the figure, in a new tab with `newTab`. The figure's slot is a fixed width, so the badges line up down a column |
699
703
  | `DataTable` | A table whose first column names each row, sortable by any column marked `sortable` | A click on a heading sorts on a wide screen. On a phone each row stacks, every fact wearing its column's label, and a dropdown sorts. A cell is a node, or `{ display, sort }` when what it shows is not what it sorts by; a formatted number in a string (`"$12,047"`, `"0.24%"`) sorts as a number. It renders in `defaultSort` on the server. Every other row sits on a tinted ground; `striped={false}` turns that off. A column marked `fit` hugs its content, for a swatch or a mark beside a figure |
700
704
  | `StatRow` | A row of large figures, each over what it counts and its `period`. `variant="quiet"` is the glance row for a working page: a light figure in the text color over a small capitalized label, hairlines between; `unit` sets a figure's unit small beside it | `trend` draws an arrow before a figure that is itself a change. `deltas` list comparisons beneath: an arrow, how far, against what (`basis`), and the earlier figure (`was`), each in its own tone, since a spend falling and a cost falling read differently |
701
705
  | `Callout` | One finding: an icon in its tone beside the title, the explainer under it | `variant="box"` sets it apart on a tinted ground, for something missing or owed |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atelic-action/ui",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Shared UI for the Atelic templates: site chrome, base styles, the component library, the access gate, and the app shell they install",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -18,8 +18,8 @@ export const gradeLabel: Record<WriteupGrade, string> = {
18
18
  /**
19
19
  * The letter as a chip in its grade's color. The documents in
20
20
  * @atelic-action/artifact (the writeup, the read, the monthly page) wear it,
21
- * so its styles live in the base sheet chain (styles/artifact.css), never in
22
- * a document's stylesheet.
21
+ * and so does ReportCard, so its styles live in styles/components.css, which
22
+ * an app may load alone; never in a document's stylesheet.
23
23
  */
24
24
  export function GradeChip({ grade }: { grade: WriteupGrade }) {
25
25
  return (
@@ -0,0 +1,37 @@
1
+ import { newTabProps } from "../lib/newTabProps";
2
+ import type { Tone } from "./tone";
3
+
4
+ export interface RankProps {
5
+ /** The figure: a rank, a place in an answer, or a dash for none. */
6
+ figure: string;
7
+ /** The tone its badge reads in. */
8
+ tone: Tone;
9
+ /** What the badge means, for a screen reader, e.g. "Top ten". */
10
+ meaning: string;
11
+ /** Where the figure leads, e.g. the page that holds the rank. */
12
+ href?: string;
13
+ /** Open the link in a new tab. */
14
+ newTab?: boolean;
15
+ className?: string;
16
+ }
17
+
18
+ /**
19
+ * A figure with its badge, for a table cell: the badge in its tone on the
20
+ * left, the figure on the right, linked when it has somewhere to go. The
21
+ * figure's slot is a fixed width, so every rank in a column is the same width
22
+ * and the badges line up down it.
23
+ */
24
+ export function Rank({ figure, tone, meaning, href, newTab, className }: RankProps) {
25
+ return (
26
+ <span className={["rank", className].filter(Boolean).join(" ")} data-tone={tone}>
27
+ <span className="chart-swatch" role="img" aria-label={meaning} />
28
+ {href ? (
29
+ <a href={href} {...newTabProps(href, newTab)}>
30
+ {figure}
31
+ </a>
32
+ ) : (
33
+ <span>{figure}</span>
34
+ )}
35
+ </span>
36
+ );
37
+ }
@@ -0,0 +1,153 @@
1
+ import { ChevronRight } from "lucide-react";
2
+ import { Fragment, type ReactNode, useId } from "react";
3
+ import { GradeChip, gradeLabel, type WriteupGrade } from "../artifact/GradeChip";
4
+
5
+ export interface ReportCardRow {
6
+ /** The row's name, e.g. a section's trade name. */
7
+ name: ReactNode;
8
+ /** Where the row leads, e.g. "#technical-seo"; the whole row is the link when set. */
9
+ href?: string;
10
+ /** The grade; a dash, read as not graded, when left out. */
11
+ grade?: WriteupGrade;
12
+ /** The earlier grade, faded beside the grade: what moved. */
13
+ before?: WriteupGrade;
14
+ /** The line behind the grade, in the owner's words. */
15
+ line?: ReactNode;
16
+ }
17
+
18
+ export interface ReportCardGroup {
19
+ /** The label over the group's rows, e.g. "Health". */
20
+ title: string;
21
+ rows: ReportCardRow[];
22
+ }
23
+
24
+ /** The words a screen reader hears around the letters. */
25
+ export interface ReportCardLabels {
26
+ /** Before a grade; "Grade". */
27
+ grade: string;
28
+ /** Before the earlier grade; "Before". */
29
+ before: string;
30
+ /** For a row with no grade; "Not graded yet". */
31
+ notGraded: string;
32
+ }
33
+
34
+ export const reportCardLabels: ReportCardLabels = {
35
+ grade: "Grade",
36
+ before: "Before",
37
+ notGraded: "Not graded yet",
38
+ };
39
+
40
+ export interface ReportCardProps {
41
+ id?: string;
42
+ /** The heading over the card, e.g. "At a Glance". */
43
+ title?: ReactNode;
44
+ /** What a screen reader calls the card when it has no title. */
45
+ label?: string;
46
+ /** What each letter means, in a few words each, right of the title. */
47
+ legend?: Array<{ grade: WriteupGrade; label: string }>;
48
+ /** Where the legend sits: beside the title, or under the card so nothing sits between a heading and the rows. */
49
+ legendAt?: "head" | "foot";
50
+ groups: ReportCardGroup[];
51
+ labels?: Partial<ReportCardLabels>;
52
+ className?: string;
53
+ }
54
+
55
+ /**
56
+ * A report card: a name, a grade, and one line on it per row, in labeled
57
+ * groups, each row's parts centered on it. A group's label
58
+ * is not a heading: the card sits inside a document whose outline the
59
+ * sections already carry. A row with an
60
+ * href is a link from edge to edge, with a chevron at its end; a link inside
61
+ * the line still goes where it points. The grade is the chip every document
62
+ * wears, and the earlier grade, when there is one, sits faded beside it.
63
+ */
64
+ export function ReportCard({
65
+ id,
66
+ title,
67
+ label,
68
+ legend,
69
+ legendAt = "head",
70
+ groups,
71
+ labels: overrides,
72
+ className,
73
+ }: ReportCardProps) {
74
+ // An override left undefined keeps the default, so a caller can pass what it has.
75
+ const titleId = useId();
76
+ const labels = { ...reportCardLabels };
77
+ for (const key of Object.keys(labels) as Array<keyof ReportCardLabels>) {
78
+ const word = overrides?.[key];
79
+ if (word !== undefined) labels[key] = word;
80
+ }
81
+ const shown = groups.filter((group) => group.rows.length > 0);
82
+ const hasLegend = legend !== undefined && legend.length > 0;
83
+ const key = hasLegend && (
84
+ <ul className={legendAt === "foot" ? "rc-legend rc-legend-foot" : "rc-legend"}>
85
+ {legend.map((entry, i) => (
86
+ // biome-ignore lint/suspicious/noArrayIndexKey: a legend is static, and two entries may share a letter
87
+ <li key={`${i}-${entry.grade}`}>
88
+ <GradeChip grade={entry.grade} />
89
+ <span>{entry.label}</span>
90
+ </li>
91
+ ))}
92
+ </ul>
93
+ );
94
+ return (
95
+ <section
96
+ className={["report-card", className].filter(Boolean).join(" ")}
97
+ id={id}
98
+ aria-labelledby={title ? titleId : undefined}
99
+ aria-label={title ? undefined : label}
100
+ >
101
+ {(title || (hasLegend && legendAt === "head")) && (
102
+ <div className="rc-head">
103
+ {title && (
104
+ <h2 className="rc-title" id={titleId}>
105
+ {title}
106
+ </h2>
107
+ )}
108
+ {legendAt === "head" && key}
109
+ </div>
110
+ )}
111
+ <div className="rc-card">
112
+ {shown.map((group, g) => (
113
+ // biome-ignore lint/suspicious/noArrayIndexKey: two groups may share a title
114
+ <Fragment key={`${g}-${group.title}`}>
115
+ <div className="rc-group">{group.title}</div>
116
+ {group.rows.map((row, i) => (
117
+ // biome-ignore lint/suspicious/noArrayIndexKey: rows are static content in a fixed order
118
+ <div className="rc-row" key={i}>
119
+ <span className="rc-grade">
120
+ {row.before && (
121
+ <span className="rc-before">
122
+ <span className="rc-sr">{`${labels.before}: ${gradeLabel[row.before]}. `}</span>
123
+ <span aria-hidden="true">
124
+ <GradeChip grade={row.before} />
125
+ </span>
126
+ </span>
127
+ )}
128
+ {row.grade ? (
129
+ <>
130
+ <span className="rc-sr">{`${labels.grade} ${gradeLabel[row.grade]}: `}</span>
131
+ <span aria-hidden="true">
132
+ <GradeChip grade={row.grade} />
133
+ </span>
134
+ </>
135
+ ) : (
136
+ <>
137
+ <span className="rc-sr">{`${labels.notGraded}: `}</span>
138
+ <span className="rc-dash" aria-hidden="true" />
139
+ </>
140
+ )}
141
+ </span>
142
+ <b className="rc-name">{row.href ? <a href={row.href}>{row.name}</a> : row.name}</b>
143
+ {row.line != null && <span className="rc-line">{row.line}</span>}
144
+ {row.href && <ChevronRight className="rc-go" size={18} aria-hidden="true" />}
145
+ </div>
146
+ ))}
147
+ </Fragment>
148
+ ))}
149
+ </div>
150
+ {legendAt === "foot" && key}
151
+ </section>
152
+ );
153
+ }
@@ -25,6 +25,15 @@ export {
25
25
  export { GateLogin, type GateLoginProps } from "./GateLogin";
26
26
  export { IconList, type IconListItem, type IconListProps } from "./IconList";
27
27
  export { NotFound, type NotFoundProps } from "./NotFound";
28
+ export { Rank, type RankProps } from "./Rank";
29
+ export {
30
+ ReportCard,
31
+ type ReportCardGroup,
32
+ type ReportCardLabels,
33
+ type ReportCardProps,
34
+ type ReportCardRow,
35
+ reportCardLabels,
36
+ } from "./ReportCard";
28
37
  export { StackedBar, type StackedBarProps, type StackedBarSegment } from "./StackedBar";
29
38
  export { describeStarRating, StarRating, type StarRatingProps } from "./StarRating";
30
39
  export {
@@ -1,5 +1,6 @@
1
1
  /* The pieces the artifact documents are built from, under src/artifact: the
2
- grade chip, the sign off, the shot, and the shell's logo and print rules. The
2
+ sign off, the shot, and the shell's logo and print rules (the grade chip's
3
+ rules sit in components.css, since ReportCard wears it there). The
3
4
  composed documents (the writeup, the read, the monthly page, the note,
4
5
  and the sealed document stage) and their sheet, with both @page rules,
5
6
  live in @atelic-action/artifact (its styles/documents.css), which a site
@@ -8,58 +9,17 @@
8
9
  a layer still wins over it whatever its specificity. */
9
10
  @layer atelic-ui {
10
11
  /* =================================================================
11
- SHARED: the vocabulary every document wears, the grade chip
12
- (GradeChip), the sign off lockup (SignOff), and the shot (Shot). A
12
+ SHARED: the vocabulary every document wears, the sign off lockup
13
+ (SignOff) and the shot (Shot). A
13
14
  class used by more than one document lives in the base sheet chain
14
15
  rather than in a component's stylesheet, so it can never vanish from
15
16
  a page that does not import the component that defined it.
16
17
 
17
- Tokens only, with one deliberate literal set: the four grade colors.
18
+ Tokens only.
18
19
  ================================================================= */
19
20
 
20
- /* ---- the grade chip ---- */
21
- .atelic-ui .op-grade {
22
- display: inline-block;
23
- vertical-align: 0.1em;
24
- margin-left: 0.6em;
25
- padding: 0.1em 0.55em 0.12em;
26
- border: 1px solid var(--hairline-strong);
27
- border-radius: 999px;
28
- font-family: var(--font-mono);
29
- font-size: 7pt;
30
- font-weight: 600;
31
- letter-spacing: 0.08em;
32
- text-transform: uppercase;
33
- color: var(--ink-soft);
34
- line-height: 1.3;
35
- min-width: 1.6em;
36
- text-align: center;
37
- }
38
- /* The four grade colors are a deliberate literal set: a grade reads
39
- green, yellow, orange, red
40
- everywhere, whatever the brand's palette, so they are not theme tokens.
41
- The orange sits between the yellow's hue and the red's so the four stay
42
- apart on paper, and its letter clears AA on its own tint. */
43
- .atelic-ui .op-grade[data-grade="a"] {
44
- color: #1b7f3b;
45
- border-color: #1b7f3b;
46
- background: #eaf6ee;
47
- }
48
- .atelic-ui .op-grade[data-grade="b"] {
49
- color: #9a6b00;
50
- border-color: #c99a1a;
51
- background: #fdf5dc;
52
- }
53
- .atelic-ui .op-grade[data-grade="c"] {
54
- color: #a84500;
55
- border-color: #e8650a;
56
- background: #fff0e3;
57
- }
58
- .atelic-ui .op-grade[data-grade="d"] {
59
- color: #b3261e;
60
- border-color: #b3261e;
61
- background: #fdecea;
62
- }
21
+ /* The grade chip's rules, its phone size included, live in components.css,
22
+ since ReportCard wears it there and an app may load that sheet alone. */
63
23
 
64
24
  /* ---- the sign off lockup: its container sets the size and the color ---- */
65
25
  .atelic-ui .op-who {
@@ -93,9 +53,6 @@
93
53
  }
94
54
 
95
55
  @media screen and (max-width: 899px) {
96
- .atelic-ui .op-grade {
97
- font-size: 10px;
98
- }
99
56
  .atelic-ui .op-sign {
100
57
  font-size: 22px;
101
58
  }
@@ -417,6 +417,275 @@
417
417
  color: var(--tone, currentColor);
418
418
  }
419
419
 
420
+ /* ---- GradeChip: the letter in its grade's color. The four colors are a
421
+ deliberate literal set, green, yellow, orange, red, whatever the
422
+ brand's palette, so they are not theme tokens. It lives here rather
423
+ than in artifact.css because ReportCard wears it, and an app may load
424
+ this sheet alone. ---- */
425
+ .atelic-ui .op-grade {
426
+ display: inline-block;
427
+ vertical-align: 0.1em;
428
+ margin-left: 0.6em;
429
+ padding: 0.1em 0.55em 0.12em;
430
+ border: 1px solid var(--hairline-strong);
431
+ border-radius: 999px;
432
+ font-family: var(--font-mono);
433
+ font-size: 7pt;
434
+ font-weight: 600;
435
+ letter-spacing: 0.08em;
436
+ text-transform: uppercase;
437
+ color: var(--ink-soft);
438
+ line-height: 1.3;
439
+ min-width: 1.6em;
440
+ text-align: center;
441
+ }
442
+ /* The orange sits between the yellow's hue and the red's so the four
443
+ stay apart on paper, and its letter clears AA on its own tint. */
444
+ .atelic-ui .op-grade[data-grade="a"] {
445
+ color: #1b7f3b;
446
+ border-color: #1b7f3b;
447
+ background: #eaf6ee;
448
+ }
449
+ .atelic-ui .op-grade[data-grade="b"] {
450
+ color: #9a6b00;
451
+ border-color: #c99a1a;
452
+ background: #fdf5dc;
453
+ }
454
+ .atelic-ui .op-grade[data-grade="c"] {
455
+ color: #a84500;
456
+ border-color: #e8650a;
457
+ background: #fff0e3;
458
+ }
459
+ .atelic-ui .op-grade[data-grade="d"] {
460
+ color: #b3261e;
461
+ border-color: #b3261e;
462
+ background: #fdecea;
463
+ }
464
+ @media screen and (max-width: 899px) {
465
+ .atelic-ui .op-grade {
466
+ font-size: 10px;
467
+ }
468
+ }
469
+
470
+ /* ---- ReportCard: a name, a grade, and a line per row, in labeled
471
+ groups ---- */
472
+ .atelic-ui .report-card {
473
+ min-width: 0;
474
+ }
475
+ .atelic-ui .rc-head {
476
+ display: flex;
477
+ flex-wrap: wrap;
478
+ align-items: center;
479
+ justify-content: space-between;
480
+ gap: 8px 20px;
481
+ margin-bottom: 10px;
482
+ }
483
+ .atelic-ui .rc-title {
484
+ margin: 0;
485
+ font-family: var(--font-mono);
486
+ font-size: 13px;
487
+ font-weight: 500;
488
+ letter-spacing: 0.1em;
489
+ text-transform: uppercase;
490
+ color: var(--neutral-500);
491
+ }
492
+ .atelic-ui .rc-legend {
493
+ list-style: none;
494
+ margin: 0 0 0 auto;
495
+ padding: 0;
496
+ display: flex;
497
+ flex-wrap: wrap;
498
+ gap: 6px 16px;
499
+ font-size: 13.5px;
500
+ color: var(--ink-soft);
501
+ }
502
+ .atelic-ui .rc-legend-foot {
503
+ justify-content: flex-end;
504
+ margin: 10px 0 0;
505
+ padding: 0 4px;
506
+ }
507
+ .atelic-ui .rc-legend li {
508
+ display: inline-flex;
509
+ align-items: center;
510
+ gap: 7px;
511
+ }
512
+ .atelic-ui .rc-legend .op-grade {
513
+ display: inline-grid;
514
+ place-items: center;
515
+ width: 22px;
516
+ height: 22px;
517
+ margin-left: 0;
518
+ padding: 0;
519
+ border-radius: 6px;
520
+ font-size: 12px;
521
+ line-height: 1;
522
+ }
523
+ .atelic-ui .rc-card {
524
+ display: grid;
525
+ grid-template-columns: auto minmax(0, 1fr) auto;
526
+ background: var(--ui-card, var(--card));
527
+ border: 1px solid var(--neutral-100);
528
+ border-radius: var(--radius-md);
529
+ box-shadow: var(--shadow-sm, none);
530
+ overflow: hidden;
531
+ }
532
+ .atelic-ui .rc-group {
533
+ grid-column: 1 / -1;
534
+ margin: 0;
535
+ padding: 7px 16px;
536
+ background: var(--surface-alt, var(--neutral-100));
537
+ font-family: var(--font-mono);
538
+ font-size: 11.5px;
539
+ font-weight: 500;
540
+ letter-spacing: 0.1em;
541
+ text-transform: uppercase;
542
+ color: var(--ink-soft);
543
+ }
544
+ /* A row is a subgrid of the card, so its parts line up down the card. On
545
+ a phone the name sits over the line; wide, they sit side by side. The
546
+ name's link stretches over the whole row. */
547
+ .atelic-ui .rc-row {
548
+ position: relative;
549
+ grid-column: 1 / -1;
550
+ display: grid;
551
+ grid-template-columns: subgrid;
552
+ grid-template-rows: auto auto;
553
+ column-gap: 12px;
554
+ row-gap: 2px;
555
+ align-items: center;
556
+ align-content: center;
557
+ padding: 12px 12px 12px 16px;
558
+ border-top: 1px solid var(--neutral-100);
559
+ transition: background-color 120ms ease;
560
+ }
561
+ .atelic-ui .rc-grade {
562
+ grid-row: 1 / span 2;
563
+ display: inline-flex;
564
+ align-items: center;
565
+ gap: 6px;
566
+ }
567
+ .atelic-ui .rc-grade .op-grade {
568
+ display: inline-grid;
569
+ place-items: center;
570
+ width: 30px;
571
+ height: 30px;
572
+ margin-left: 0;
573
+ padding: 0;
574
+ border-radius: 9px;
575
+ font-size: 15px;
576
+ line-height: 1;
577
+ }
578
+ .atelic-ui .rc-before .op-grade {
579
+ opacity: 0.45;
580
+ }
581
+ .atelic-ui .rc-dash {
582
+ display: inline-flex;
583
+ align-items: center;
584
+ justify-content: center;
585
+ width: 30px;
586
+ height: 30px;
587
+ color: var(--neutral-500);
588
+ }
589
+ .atelic-ui .rc-dash::before {
590
+ content: "";
591
+ width: 12px;
592
+ border-top: 2px solid currentColor;
593
+ }
594
+ .atelic-ui .rc-name {
595
+ grid-column: 2;
596
+ font-size: 16px;
597
+ font-weight: 650;
598
+ letter-spacing: -0.01em;
599
+ line-height: 1.35;
600
+ color: var(--ink);
601
+ }
602
+ .atelic-ui .rc-name a {
603
+ color: inherit;
604
+ text-decoration: none;
605
+ }
606
+ .atelic-ui .rc-name a::after {
607
+ content: "";
608
+ position: absolute;
609
+ inset: 0;
610
+ }
611
+ .atelic-ui .rc-line {
612
+ grid-column: 2;
613
+ font-size: 15px;
614
+ line-height: 1.45;
615
+ color: var(--ink-soft);
616
+ text-wrap: pretty;
617
+ }
618
+ .atelic-ui .rc-line a {
619
+ position: relative;
620
+ z-index: 1;
621
+ color: var(--ui-primary, var(--primary));
622
+ text-decoration: underline;
623
+ text-underline-offset: 2px;
624
+ }
625
+ .atelic-ui .rc-go {
626
+ grid-column: 3;
627
+ grid-row: 1 / span 2;
628
+ color: var(--neutral-500);
629
+ }
630
+ .atelic-ui .rc-sr {
631
+ position: absolute;
632
+ width: 1px;
633
+ height: 1px;
634
+ margin: -1px;
635
+ padding: 0;
636
+ overflow: hidden;
637
+ clip: rect(0, 0, 0, 0);
638
+ white-space: nowrap;
639
+ border: 0;
640
+ }
641
+ @media print, (min-width: 721px) {
642
+ .atelic-ui .rc-card {
643
+ grid-template-columns: auto 232px minmax(0, 1fr) auto;
644
+ }
645
+ .atelic-ui .rc-group {
646
+ padding: 7px 20px;
647
+ }
648
+ .atelic-ui .rc-row {
649
+ grid-template-rows: auto;
650
+ column-gap: 16px;
651
+ padding: 11px 14px 11px 20px;
652
+ }
653
+ .atelic-ui .rc-grade,
654
+ .atelic-ui .rc-go {
655
+ grid-row: 1;
656
+ }
657
+ .atelic-ui .rc-line {
658
+ grid-column: 3;
659
+ }
660
+ .atelic-ui .rc-go {
661
+ grid-column: 4;
662
+ }
663
+ }
664
+ @media print {
665
+ .atelic-ui .rc-card {
666
+ box-shadow: none;
667
+ border-color: var(--neutral-200);
668
+ }
669
+ .atelic-ui .rc-go {
670
+ display: none;
671
+ }
672
+ }
673
+ @media (prefers-reduced-motion: reduce) {
674
+ .atelic-ui .rc-row {
675
+ transition: none;
676
+ }
677
+ }
678
+ .atelic-ui .rc-row:has(.rc-name a):hover,
679
+ .atelic-ui .rc-row:has(.rc-name a):focus-within {
680
+ background: color-mix(in srgb, var(--ui-primary, var(--primary)) 6%, transparent);
681
+ }
682
+ .atelic-ui .rc-row:has(.rc-name a):hover .rc-name,
683
+ .atelic-ui .rc-row:has(.rc-name a):focus-within .rc-name,
684
+ .atelic-ui .rc-row:has(.rc-name a):hover .rc-go,
685
+ .atelic-ui .rc-row:has(.rc-name a):focus-within .rc-go {
686
+ color: var(--ui-primary, var(--primary));
687
+ }
688
+
420
689
  /* ---- The charts' shared key: a swatch, a label, a figure ---- */
421
690
  .atelic-ui .chart-key {
422
691
  list-style: none;
@@ -447,6 +716,24 @@
447
716
  flex: none;
448
717
  }
449
718
 
719
+ /* ---- Rank: a figure with its badge; the figure's slot is a fixed
720
+ width, so every rank in a column is the same width and the badges
721
+ line up down it ---- */
722
+ .atelic-ui .rank {
723
+ display: inline-grid;
724
+ grid-template-columns: auto 2.6em;
725
+ column-gap: 8px;
726
+ align-items: center;
727
+ }
728
+ .atelic-ui .rank .chart-swatch {
729
+ justify-self: start;
730
+ }
731
+ .atelic-ui .rank > a,
732
+ .atelic-ui .rank > span:last-child {
733
+ justify-self: end;
734
+ font-variant-numeric: tabular-nums;
735
+ }
736
+
450
737
  /* ---- StackedBar: one bar, the parts of a whole ---- */
451
738
  .atelic-ui .stacked-bar-track {
452
739
  display: flex;