@bezda/rhp-mcp 0.0.0-stage → 0.1.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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +221 -2
  3. package/bin/rhp-mcp.js +125 -0
  4. package/package.json +62 -4
  5. package/skill/SKILL.md +363 -0
  6. package/skill/recipes/area.html +150 -0
  7. package/skill/recipes/bar.html +139 -0
  8. package/skill/recipes/box-plot.html +158 -0
  9. package/skill/recipes/bubble.html +182 -0
  10. package/skill/recipes/bullet.html +132 -0
  11. package/skill/recipes/candlestick.html +173 -0
  12. package/skill/recipes/column.html +125 -0
  13. package/skill/recipes/diverging-bars.html +144 -0
  14. package/skill/recipes/donut.html +225 -0
  15. package/skill/recipes/dumbbell.html +131 -0
  16. package/skill/recipes/gantt.html +186 -0
  17. package/skill/recipes/grouped-bars.html +136 -0
  18. package/skill/recipes/heatmap.html +177 -0
  19. package/skill/recipes/histogram.html +193 -0
  20. package/skill/recipes/line.html +164 -0
  21. package/skill/recipes/live.html +249 -0
  22. package/skill/recipes/lollipop.html +146 -0
  23. package/skill/recipes/multi-line.html +223 -0
  24. package/skill/recipes/pyramid.html +156 -0
  25. package/skill/recipes/race.html +272 -0
  26. package/skill/recipes/radial-bars.html +151 -0
  27. package/skill/recipes/scatter.html +193 -0
  28. package/skill/recipes/slope.html +193 -0
  29. package/skill/recipes/sparklines.html +169 -0
  30. package/skill/recipes/stacked-100.html +152 -0
  31. package/skill/recipes/stacked-bars.html +144 -0
  32. package/skill/recipes/strip.html +215 -0
  33. package/skill/recipes/violin.html +163 -0
  34. package/skill/recipes/waffle.html +159 -0
  35. package/skill/recipes/waterfall.html +121 -0
  36. package/skill/references/api.md +1556 -0
  37. package/skill/references/design.md +763 -0
  38. package/skill/references/environments.md +1570 -0
  39. package/skill/references/forms.md +160 -0
  40. package/skill/references/interaction.md +792 -0
  41. package/skill/references/pitfalls.md +157 -0
  42. package/src/check/README.md +163 -0
  43. package/src/check/browser.js +92 -0
  44. package/src/check/build.js +439 -0
  45. package/src/check/colors.js +324 -0
  46. package/src/check/findings.js +247 -0
  47. package/src/check/index.js +419 -0
  48. package/src/check/inpage.js +1268 -0
  49. package/src/check/interact.js +266 -0
  50. package/src/check/png.js +98 -0
  51. package/src/check/report.js +103 -0
  52. package/src/check/source.js +387 -0
  53. package/src/check/sourcemap.js +75 -0
  54. package/src/guard/note.js +30 -0
  55. package/src/guard/render.js +17 -0
  56. package/src/guard/rhp.js +608 -0
  57. package/src/guard/rules.js +125 -0
  58. package/src/server.js +265 -0
  59. package/src/skill.js +140 -0
@@ -0,0 +1,159 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Blood types of 100 people</title>
7
+ <meta name="description" content="Waffle chart: 100 units in a 10 by 10 grid, one per person, colored by group, with a picker that lights the units a chosen group relates to. Use it for counts out of 100 or shares of a population.">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Public+Sans:wght@400..800&display=swap">
11
+ <script type="importmap">{ "imports": { "@bezda/rhp/standalone": "https://cdn.jsdelivr.net/npm/@bezda/rhp@2/dist/standalone.js" } }</script>
12
+ <style>
13
+ * { box-sizing: border-box; }
14
+ body { margin: 0; padding: 48px 16px; background: #dddbd6; font-family: "Public Sans", "Helvetica Neue", Arial, sans-serif; }
15
+ /* The poster: a donor card on white card stock, torn off along its perforated top edge */
16
+ .poster { position: relative; display: grid; max-width: 980px; margin: 0 auto; padding: 50px 48px 30px; color: #1b1d21;
17
+ background: repeating-linear-gradient(0deg, rgb(27 29 33 / .035) 0 1px, transparent 1px 28px), #fbfbf9;
18
+ border-radius: 4px 4px 14px 14px; box-shadow: 0 1px 2px rgb(40 38 34 / .16), 0 24px 44px -26px rgb(40 38 34 / .5); }
19
+ .poster > * { min-width: 0; }
20
+ .poster::before { content: ""; position: absolute; top: 18px; left: 22px; right: 22px; height: 6px;
21
+ background: radial-gradient(circle, #dddbd6 2.4px, transparent 2.9px) 0 50% / 14px 6px space no-repeat; }
22
+ .poster figcaption { display: grid; gap: 12px; margin-bottom: 24px; }
23
+ .poster .kicker { font-size: 12px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; color: #a3202f; }
24
+ .poster .headline { font-size: 46px; font-weight: 800; line-height: 1.02; letter-spacing: -.03em; text-wrap: balance; }
25
+ .poster .dek { max-width: 46ch; font-size: 16px; line-height: 1.5; color: #4a4d53; }
26
+ .poster .note { display: block; margin-top: 22px; font-size: 12px; line-height: 1.5; color: #62656b; }
27
+ /* The picker: one button per type, with its drop and how many of the 100 have it */
28
+ .types { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; max-width: 420px; }
29
+ .types button { display: flex; align-items: center; gap: 7px; padding: 8px 10px; border: 1.5px solid #d3d1cb; border-radius: 10px; background: #fff;
30
+ font: 700 15px/1 "Public Sans", Arial, sans-serif; color: #1b1d21; cursor: pointer; }
31
+ .types button span { margin-left: auto; font-weight: 500; font-size: 13px; color: #62656b; font-variant-numeric: tabular-nums; }
32
+ .types button[aria-pressed="true"] { border-color: #1b1d21; box-shadow: inset 0 0 0 1px #1b1d21; }
33
+ .types button:hover { border-color: #8d8f94; }
34
+ .types button:focus-visible { outline: 2px solid #1b1d21; outline-offset: 2px; }
35
+ .types svg { flex: none; width: 14px; height: 17px; fill: var(--c); fill-rule: evenodd; }
36
+ .readout { margin: 16px 0 8px; font-size: 16px; font-weight: 700; line-height: 1.3; min-height: 1.3em; }
37
+ .chart { width: 100%; max-width: 470px; justify-self: center; margin-top: 8px; }
38
+ @media (min-width: 860px) {
39
+ .poster { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-rows: auto auto auto 1fr; column-gap: 52px; }
40
+ .poster figcaption { grid-column: 1; grid-row: 1; }
41
+ .types { grid-column: 1; grid-row: 2; }
42
+ .readout { grid-column: 1; grid-row: 3; }
43
+ .poster .chart { grid-column: 2; grid-row: 1 / span 4; align-self: center; margin-top: 0; }
44
+ .poster .note { grid-column: 1; grid-row: 4; align-self: end; }
45
+ }
46
+ @media (max-width: 520px) {
47
+ body { padding: 24px 16px 32px; }
48
+ .poster { padding: 42px 18px 20px; }
49
+ .poster .headline { font-size: 33px; }
50
+ .poster .dek { font-size: 15px; }
51
+ .types button { padding: 8px 7px; gap: 5px; font-size: 14px; }
52
+ .readout { font-size: 15px; }
53
+ }
54
+ </style>
55
+ </head>
56
+ <body>
57
+ <div id="chart"></div>
58
+ <script type="module">
59
+ import { Chart, Plot, Cell, Poster, slat, shape, html, render, createSignal, For } from "@bezda/rhp/standalone";
60
+
61
+ // Data: the blood types of 100 people in the US, in the order they fill the grid: Stanford Blood Center's shares of the
62
+ // US population (from the AABB Technical Manual, 18th edition, 2014), rounded to whole people that add up to 100.
63
+ // Replace with your own counts; they must add up to 100.
64
+ const TYPES = [
65
+ { type: "O+", people: 37 },
66
+ { type: "O−", people: 7 },
67
+ { type: "A+", people: 36 },
68
+ { type: "A−", people: 6 },
69
+ { type: "B+", people: 8 },
70
+ { type: "B−", people: 2 },
71
+ { type: "AB+", people: 3 },
72
+ { type: "AB−", people: 1 },
73
+ ];
74
+ const GROUP = { O: "#c62a3a", A: "#3b6fd8", B: "#e39a13", AB: "#16a085" };
75
+ const FADED = "#e4e2dc";
76
+ const TOTAL = TYPES.reduce((sum, t) => sum + t.people, 0);
77
+ const PEOPLE = TYPES.flatMap((t, i) => Array(t.people).fill(i)); // one type number per person
78
+ const LINES = Array.from({ length: 10 }, (_, k) => PEOPLE.slice(k * 10, k * 10 + 10)); // ten people per line
79
+
80
+ const colorOf = (t) => GROUP[t.type.slice(0, -1)];
81
+ const negative = (t) => t.type.endsWith("−");
82
+ // Red cells can go to anyone who has every antigen (A, B, Rh) the donor's cells have
83
+ const canGive = (from, to) => ["A", "B", "+"].every((k) => !from.includes(k) || to.includes(k));
84
+ const reach = (i) => TYPES.filter((t) => canGive(TYPES[i].type, t.type)).reduce((sum, t) => sum + t.people, 0);
85
+
86
+ // A blood drop for a Cell, in the Cell's own box. The first number runs along the value axis, which in these
87
+ // cells (a Plot across a flat chart) runs up, so the tip is at 0.92.
88
+ // The same drop for the picker's buttons, as an SVG path (a hole for Rh-negative)
89
+ const SWATCH = "M7 1C8.6 3.3 12.6 7 12.6 10.9A5.6 5.6 0 0 1 1.4 10.9C1.4 7 5.4 3.3 7 1Z";
90
+ const HOLE = "M7 8.4A2.5 2.5 0 1 0 7 13.4A2.5 2.5 0 1 0 7 8.4Z";
91
+ const DROP = shape(["M", 0.92, 0.5], ["C", 0.78, 0.6, 0.58, 0.8, 0.38, 0.8], ["C", 0.214, 0.8, 0.08, 0.666, 0.08, 0.5],
92
+ ["C", 0.08, 0.334, 0.214, 0.2, 0.38, 0.2], ["C", 0.58, 0.2, 0.78, 0.4, 0.92, 0.5], ["Z"]);
93
+
94
+ // Slat types: a person is one drop, colored by group and hollow when Rh-negative; a line is ten people side by side
95
+ const Person = slat({
96
+ css: `
97
+ .drop { --rhp-cell-gap: 0px; --rhp-radius: 0px; cursor: pointer; }
98
+ .neg { background: radial-gradient(circle closest-side at 50% 62%, #fbfbf9 50%, var(--rhp-color) 53%); }
99
+ `,
100
+ }, (p) => html`
101
+ <div data-type=${() => p.type}>
102
+ <${Cell} shape=${DROP} class=${() => (negative(TYPES[p.type]) ? "drop neg" : "drop")}
103
+ color=${() => (p.active == null || canGive(TYPES[p.active].type, TYPES[p.type].type) ? colorOf(TYPES[p.type]) : FADED)} />
104
+ </div>`);
105
+
106
+ const TenPeople = slat({ room: { start: 0, end: 0 } }, (d) => html`
107
+ <div><${Plot} orientation="across" type=${() => d.people} active=${() => d.active}>${Person}<//></div>`);
108
+
109
+ // Chart component: a type picked with its button or by tapping a drop (or previewed under a mouse or a pen) lights
110
+ // everyone its red cells can be given to
111
+ function BloodTypes() {
112
+ const [picked, setPicked] = createSignal(null);
113
+ const [preview, setPreview] = createSignal(null);
114
+ const active = () => preview() ?? picked();
115
+ const typeAt = (e) => {
116
+ const el = e.target.closest("[data-type]");
117
+ return el ? +el.dataset.type : null;
118
+ };
119
+ const pick = (i) => setPicked(picked() === i ? null : i);
120
+ // A mouse or a pen previews the type under it; between the drops the preview stays, and off the chart (on the
121
+ // buttons) it ends, so the picked type shows
122
+ const hover = (e) => {
123
+ if (e.pointerType === "touch") return;
124
+ if (typeAt(e) != null) setPreview(typeAt(e));
125
+ else if (!e.target.closest(".chart")) setPreview(null);
126
+ };
127
+ const readout = () => {
128
+ const i = active();
129
+ if (i == null) return "Pick a type to see who can receive it.";
130
+ const n = reach(i);
131
+ return `${TYPES[i].type} blood can go to ${n === TOTAL ? "all" : n + " of"} ${TOTAL} people`;
132
+ };
133
+ return html`
134
+ <${Poster}
135
+ kicker="Blood types · 100 people"
136
+ title="Seven in 100 Americans have the blood anyone can receive"
137
+ dek="Each drop is one person, colored by blood group, and a hollow drop is Rh-negative. Only O− red cells can be given to every type, so its donors are always in demand."
138
+ note="Source: Stanford Blood Center, shares of the US population (from the AABB Technical Manual, 18th edition, 2014), rounded to whole people. Compatibility of red cells. Point at a drop, tap it, or pick a type."
139
+ onPointerMove=${hover}
140
+ onPointerLeave=${(e) => setPreview(null)}
141
+ onClick=${(e) => typeAt(e) != null && pick(typeAt(e))}>
142
+ <div class="types" role="group" aria-label="Blood type">
143
+ <${For} each=${TYPES}>${(t, i) => html`
144
+ <button type="button" aria-pressed=${() => picked() === i()} onClick=${() => pick(i())}>
145
+ <svg viewBox="0 0 14 17" aria-hidden="true" style=${{ "--c": colorOf(t) }}><path d=${negative(t) ? SWATCH + HOLE : SWATCH} /></svg>${t.type}<span>${t.people}</span>
146
+ </button>`}<//>
147
+ </div>
148
+ <p class="readout" aria-live="polite">${readout}</p>
149
+ <${Chart} class="chart" aspect=${1} ticks=${false} label="Blood types of 100 people in the US, one drop per person"
150
+ theme=${{ font: "'Public Sans', 'Helvetica Neue', Arial, sans-serif", ink: "#1b1d21", surface: "#fbfbf9" }}>
151
+ <${Plot} people=${LINES} active=${active}>${TenPeople}<//>
152
+ <//>
153
+ <//>`;
154
+ }
155
+
156
+ render(() => html`<${BloodTypes} />`, document.getElementById("chart"));
157
+ </script>
158
+ </body>
159
+ </html>
@@ -0,0 +1,121 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Profit bridge</title>
7
+ <meta name="description" content="Waterfall chart from a starting value through gains and losses to a final total, each step linked to the next, with a switch between periods. Use it to show how one amount becomes another, step by step.">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=DM+Sans:opsz,wght@9..40,400..700&family=DM+Serif+Display&display=swap">
11
+ <script type="importmap">{ "imports": { "@bezda/rhp/standalone": "https://cdn.jsdelivr.net/npm/@bezda/rhp@2/dist/standalone.js" } }</script>
12
+ <style>
13
+ * { box-sizing: border-box; }
14
+ body { margin: 0; padding: 48px 16px; background: #d6d3cb; font-family: "DM Sans", "Segoe UI", system-ui, sans-serif; }
15
+ /* The poster: an accountant's ledger, serif headline, figures in mono, one ruled line per step, the bottom line double-ruled */
16
+ .poster { max-width: 900px; margin: 0 auto; padding: 46px 52px 30px; color: #2a2f38; border-radius: 3px; background: #f6f3ea;
17
+ box-shadow: 0 26px 50px -30px rgb(62 58 50 / .6); }
18
+ .ledger { background: repeating-linear-gradient(transparent 0 49px, rgb(42 47 56 / .12) 49px 50px) 0 2px; }
19
+ .poster figcaption { display: grid; gap: 12px; margin-bottom: 24px; }
20
+ .poster .kicker { font: 500 12px/1.3 "DM Mono", ui-monospace, Menlo, monospace; letter-spacing: .14em; text-transform: uppercase; color: #646b78; }
21
+ .poster .headline { max-width: 15em; font: 400 52px/1.02 "DM Serif Display", Georgia, serif; letter-spacing: -.01em; text-wrap: balance; }
22
+ .poster .dek { max-width: 56ch; font-size: 16px; line-height: 1.55; color: #4f5562; }
23
+ .poster .note { display: block; margin-top: 22px; font-size: 12px; line-height: 1.5; color: #646b78; }
24
+ /* The quarter switch, as ledger tabs, and the one-line readout of the quarter shown */
25
+ .quarters { display: inline-flex; border: 1.5px solid #2a2f38; border-radius: 6px; overflow: hidden; }
26
+ .quarters button { padding: 8px 16px; border: 0; border-left: 1.5px solid #2a2f38; background: #f6f3ea; color: #2a2f38; font: 500 14px/1 "DM Mono", ui-monospace, Menlo, monospace; cursor: pointer; }
27
+ .quarters button:first-child { border-left: 0; }
28
+ .quarters button[aria-pressed="true"] { background: #2a2f38; color: #f6f3ea; }
29
+ .quarters button:focus-visible { outline: 3px solid #2457c5; outline-offset: -3px; }
30
+ .readout { min-height: 1lh; margin: 14px 0 18px; font-size: 15px; line-height: 1.45; color: #4f5562; }
31
+ .readout b { font: 500 17px/1 "DM Mono", ui-monospace, Menlo, monospace; color: #1f4fae; }
32
+ @media (max-width: 600px) {
33
+ body { padding: 16px 16px 32px; }
34
+ .poster { padding: 26px 16px 18px; }
35
+ .poster .headline { font-size: 34px; }
36
+ .poster .dek { font-size: 15px; }
37
+ .readout { min-height: 2lh; }
38
+ }
39
+ </style>
40
+ </head>
41
+ <body>
42
+ <div id="chart"></div>
43
+ <script type="module">
44
+ import { Chart, Plot, Bar, Tick, Label, Poster, Show, slat, nice, running, html, render, createSignal, createMemo } from "@bezda/rhp/standalone";
45
+
46
+ // Data: a company's quarters in millions of dollars, revenue first, then each cost as a negative. Illustrative data; replace with your own.
47
+ const ITEMS = ["Revenue", "Cost of sales", "Research", "Sales & marketing", "Admin", "Tax"];
48
+ const TOTAL = "Net profit"; // the last step: what is left
49
+ const QUARTERS = [
50
+ { name: "Q1", amounts: [120, -52, -22, -26, -9, -3] },
51
+ { name: "Q2", amounts: [128, -54, -23, -24, -9, -4] },
52
+ { name: "Q3", amounts: [135, -56, -24, -21, -10, -5] },
53
+ { name: "Q4", amounts: [151, -61, -25, -22, -10, -7] },
54
+ ];
55
+
56
+ const compact = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", notation: "compact", maximumFractionDigits: 0 });
57
+ const money = (m) => compact.format(m * 1e6).replace("-", "−"); // -61 -> −$61M, with a real minus sign
58
+ const percent = new Intl.NumberFormat("en-US", { style: "percent" });
59
+ const scale = nice(0, Math.max(...QUARTERS.map((q) => q.amounts[0])) * 1.15, 4); // 0 to $200M, room for the labels
60
+
61
+ // Slat type: a step from the running total before it to the one after, a hairline down to the next step, and its amount
62
+ const Step = slat({
63
+ thickness: 50,
64
+ inset: "12px",
65
+ room: { start: "auto", end: 24 }, // the end: half of the last axis number, "$200M"
66
+ css: `
67
+ .item { font-size: 15px; font-weight: 600; --rhp-label-gap: 14px; }
68
+ .bar { --rhp-radius: 2px; }
69
+ .in .bar { background: #5d6676; }
70
+ .out .bar { background: #c9472b; }
71
+ .total .bar { background: #2457c5; }
72
+ .link { background: #8b909a; --rhp-tick-width: 1px; }
73
+ .link:horizontal { top: calc(100% - 12px); height: 24px; translate: -50% 0; }
74
+ .link:vertical { left: calc(100% - 12px); width: 24px; translate: 0 50%; }
75
+ .amount { font: 500 13px/1 "DM Mono", ui-monospace, Menlo, monospace; --rhp-label-gap: 8px; }
76
+ .out .amount { color: #b23a22; }
77
+ .total .amount { color: #1f4fae; font-size: 14px; text-decoration: underline double; text-underline-offset: 5px; }
78
+ @container (max-width: 420px) { .item { max-width: 6.5em; font-size: 13px; line-height: 1.1; white-space: normal; --rhp-label-gap: 8px; } .amount { font-size: 12px; } }
79
+ `,
80
+ }, (d) => html`
81
+ <div class=${() => "step " + d.kind}>
82
+ <${Label} edge="start" class="item">${() => d.item}<//>
83
+ <${Bar} from=${() => d.from} to=${() => d.to} class="bar" />
84
+ <${Show} when=${() => d.kind !== "total"}><${Tick} at=${() => d.to} class="link" /><//>
85
+ <${Label} at=${() => Math.max(d.from, d.to)} class="amount">${() => money(d.to - d.from)}<//>
86
+ </div>`);
87
+
88
+ // Chart component: the quarter picked feeds running(), so every step, the profit and the readout move together
89
+ function ProfitBridge() {
90
+ const [q, setQ] = createSignal(QUARTERS.length - 1);
91
+ const quarter = () => QUARTERS[q()];
92
+ const steps = createMemo(() => {
93
+ const r = running(quarter().amounts); // each step from the total before it to the one after
94
+ return { from: [...r.from, 0], to: [...r.to, r.to.at(-1)] }; // and the profit, from 0
95
+ });
96
+ const profit = () => steps().to.at(-1);
97
+ return html`
98
+ <${Poster}
99
+ kicker="Quarterly results · profit bridge"
100
+ title="Profit tripled over the year while revenue grew by a quarter"
101
+ dek="How each quarter's revenue became profit, in millions of dollars. Each cost comes off one step at a time, and what is left at the bottom is the profit."
102
+ note="Illustrative data for a fictional company.">
103
+ <div class="quarters" role="group" aria-label="Quarter">
104
+ ${QUARTERS.map((x, i) => html`<button type="button" aria-pressed=${() => q() === i} onClick=${() => setQ(i)}>${x.name}</button>`)}
105
+ </div>
106
+ <p class="readout" aria-live="polite">
107
+ ${() => quarter().name + ": "}<b>${() => money(profit())}</b>${() => ` net profit on ${money(quarter().amounts[0])} of revenue, a margin of ${percent.format(profit() / quarter().amounts[0])}.`}
108
+ </p>
109
+ <${Chart} class="ledger" scale=${[scale.min, scale.max]} ticks=${scale.ticks} format=${money}
110
+ label=${() => "Profit bridge for " + quarter().name + ", in millions of dollars"}
111
+ theme=${{ font: '"DM Sans", "Segoe UI", system-ui, sans-serif', ink: "#2a2f38", muted: "#646b78", grid: "#dcd8cd" }}>
112
+ <${Plot} item=${[...ITEMS, TOTAL]} from=${() => steps().from} to=${() => steps().to}
113
+ kind=${(d) => (d.index === ITEMS.length ? "total" : d.to >= d.from ? "in" : "out")}>${Step}<//>
114
+ <//>
115
+ <//>`;
116
+ }
117
+
118
+ render(() => html`<${ProfitBridge} />`, document.getElementById("chart"));
119
+ </script>
120
+ </body>
121
+ </html>