meliora-consent 0.0.0-stage → 0.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/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-11-15
4
+
5
+ - **One-line install:** `dist/meliora-consent.js` is the whole banner in one file. The stored choice and Consent Mode defaults apply at once, before any tag; the banner is built when the page is parsed; `meliora-consent.css` loads from next to the file. Settings can come from `data-` attributes on the script tag (`data-categories`, `data-policy`, `data-policy-<lang>`, `data-policy-version`, `data-consent-mode`, `data-gpc`, `data-renew-days`, `data-cookie-domain`, `data-log`, `data-insert-after`, `data-css="none"`). `window.MelioraConsentConfig` still works and wins over the attributes. The separate files in `src/` still work.
6
+ - **On npm:** `npm install meliora-consent`. The package has `dist/` and `src/`, no dependencies; `import 'meliora-consent'` and `import 'meliora-consent/css'` work in bundlers. The script only loads its stylesheet by itself when it is its own file, so bundling it never requests a wrong CSS URL.
7
+ - `npm run build` regenerates `dist/`; `npm run check` fails when it is out of date.
8
+ - Meliora Accessibility 1.1.0 shows a "Cookie settings" button in its menu when this banner is on the page.
9
+ - The demo page uses the one-file build and has a snippet builder for the install.
10
+ - Tests: the whole suite now runs against the one-file build, plus tests for both packages installed with one tag each.
11
+
12
+ ## 0.1.0 — 2026-11-15
13
+
14
+ First version, built to the spec in `COOKIE-CONSENT.md` (melioradev/meliora-landing).
15
+
16
+ - Banner with Accept all, Reject all and Choose cookies, all in the same style. No ×; ignoring the banner is not consent.
17
+ - Settings dialog: one switch per category (functional, analytics, marketing), "Always on" for necessary, cookie lists, Save / Reject all / Accept all.
18
+ - Tagged scripts (`type="text/plain" data-consent`) and embeds (`iframe data-consent data-src`) are released only after consent, in document order. Embeds get an accessible placeholder.
19
+ - Withdrawing a category deletes its declared first-party cookies and storage keys, then reloads.
20
+ - Google Consent Mode v2 defaults and updates, `ads_data_redaction`, and a `meliora_consent_update` dataLayer event.
21
+ - Global Privacy Control keeps marketing off.
22
+ - Choice stored in `localStorage` (`meliora.consent`), asked again on a new policy version, a new category, or after 365 days. Optional cookie for subdomains and optional consent log (example Cloudflare Worker with D1).
23
+ - Works with the Meliora accessibility panel: card beside its button, or a sheet that lifts it; follows its High contrast, Stop animations and text size settings.
24
+ - Focus never hidden behind the banner; spacer at the end of the page while it shows.
25
+ - Ten languages, following the page's `<html lang>`. Non-English strings need a native speaker's review.
26
+ - Works under a strict Content-Security-Policy (no inline scripts or styles).
package/LICENSE ADDED
@@ -0,0 +1,45 @@
1
+ Meliora Consent License (MIT with visible attribution)
2
+
3
+ Copyright (c) 2026 Meliora Novotech Ltd (https://melioranovotech.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ 1. The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ 2. Visible attribution. Every website, web application, application or other
16
+ product that uses or includes the Software, or a work derived from it, must
17
+ display to its end users, in the cookie settings dialog or in another place
18
+ that is visible without inspecting source code:
19
+
20
+ (a) the credit "Made by Meliora Novotech Ltd", or an equivalent credit
21
+ translated into the language being displayed;
22
+ (b) a working link to https://melioranovotech.com; and
23
+ (c) a working link to https://github.com/melioradev/meliora-consent.
24
+
25
+ The credit and links must not be removed, hidden, made illegible, or made
26
+ unreachable by keyboard or assistive technology. The unmodified Software
27
+ meets this condition through the credit line at the bottom of its cookie
28
+ settings dialog.
29
+
30
+ 3. The name "Meliora Novotech" and its logos may not be used to endorse or
31
+ promote products derived from the Software without prior written permission,
32
+ except as required by condition 2.
33
+
34
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40
+ SOFTWARE.
41
+
42
+ Using the Software does not by itself make a website compliant with any
43
+ privacy, cookie or accessibility law. Compliance depends on the website's own
44
+ content, code, configuration and the services it uses. This is not legal
45
+ advice.
package/README.md CHANGED
@@ -1,3 +1,256 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="docs/favicon.png" width="72" height="72" alt="">
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ <h1 align="center">Meliora Consent</h1>
6
+
7
+ <p align="center">
8
+ An accessible cookie banner for any website.<br>
9
+ Reject as easy as accept · Google Consent Mode v2 · ten languages · no dependencies · no tracking.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/meliora-consent"><img src="https://img.shields.io/npm/v/meliora-consent?color=2557b4&label=npm" alt="npm version"></a>
14
+ <a href="https://bundlephobia.com/package/meliora-consent"><img src="https://img.shields.io/bundlephobia/minzip/meliora-consent?color=2557b4&label=min%2Bgzip" alt="Size, minified and gzipped"></a>
15
+ <img src="https://img.shields.io/badge/dependencies-0-2557b4" alt="No dependencies">
16
+ </p>
17
+
18
+ <p align="center">
19
+ <code>npm install meliora-consent</code>
20
+ </p>
21
+
22
+ <p align="center">
23
+ <a href="https://melioradev.github.io/meliora-consent/"><strong>Live demo</strong></a> ·
24
+ <a href="#install">Install</a> ·
25
+ <a href="#configuration">Configuration</a> ·
26
+ <a href="#license">License</a>
27
+ </p>
28
+
29
+ <p align="center">
30
+ <img src="docs/screenshots/banner-light.png" width="420" alt="The banner at the bottom-left of a page, with Accept all, Reject all and Choose cookies buttons in the same style">
31
+ &nbsp;
32
+ <img src="docs/screenshots/dialog-light.png" width="420" alt="The cookie settings dialog with a switch for each category and the list of its cookies">
33
+ </p>
34
+ <p align="center">
35
+ <img src="docs/screenshots/banner-dark-he.png" width="420" alt="The banner in Hebrew, right to left, in a dark theme">
36
+ &nbsp;
37
+ <img src="docs/screenshots/banner-mobile.png" width="160" alt="On a phone the banner is a full-width sheet and the accessibility button moves above it">
38
+ </p>
39
+
40
+ ---
41
+
42
+ ## What it does
43
+
44
+ - **Nothing optional runs before a choice.** Analytics, marketing and other third-party scripts stay inert until their category is allowed. Ignoring the banner is not consent.
45
+ - **Reject is as easy as accept.** "Accept all", "Reject all" and "Choose cookies" sit together on the first layer, in exactly the same style. There's no × and no "by continuing to browse…".
46
+ - **Specific choices.** The settings dialog has a switch per category (functional, analytics, marketing) and lists each category's cookies.
47
+ - **Easy to withdraw.** A "Cookie settings" button in your footer reopens the dialog. Turning a category off deletes its declared first-party cookies and reloads the page.
48
+ - **Embeds wait too.** YouTube videos, maps and other iframes get an accessible placeholder with an "Allow and show" button.
49
+ - **Google Consent Mode v2** defaults and updates, set before any Google tag loads.
50
+ - **Global Privacy Control** is honored: marketing stays off.
51
+ - **Accessible:** keyboard-first, WCAG 2.1 AA, never hides the element that has focus, works at 200% zoom and 320px width. The settings dialog is a native `<dialog>` with a focus trap and Esc.
52
+ - **Follows the page language** (`<html lang>`), with right-to-left Hebrew and Arabic.
53
+ - **Works with [Meliora Accessibility](https://github.com/melioradev/meliora-accessibility):** the banner stays clear of its button and follows its High contrast, Stop animations and text size settings.
54
+ - **No banner when there's nothing to ask.** With no optional categories configured, it does nothing.
55
+ - **Strict CSP friendly:** no inline scripts or styles, no requests to anyone but your own site.
56
+
57
+ ## Languages
58
+
59
+ English (default) · العربية · 中文(简体) · Français · Deutsch · עברית · Italiano · Русский · Español · Українська
60
+
61
+ The banner uses the page's `<html lang>` and falls back to English. All strings are in [`src/meliora-consent-i18n.js`](src/meliora-consent-i18n.js), and every one can be overridden from the config. Corrections from native speakers are very welcome.
62
+
63
+ ## Install
64
+
65
+ ```sh
66
+ npm install meliora-consent
67
+ ```
68
+
69
+ The package is one script and one stylesheet in `dist/`, with no dependencies. Use it whichever way suits your site:
70
+
71
+ **1. As a script tag (any site).** Copy `node_modules/meliora-consent/dist/meliora-consent.js` and `meliora-consent.css` into the same folder of your site, and add one line to `<head>`, **without** `defer`, before any tracking script. The script loads the stylesheet next to it by itself:
72
+
73
+ ```html
74
+ <script src="/js/meliora-consent.js"
75
+ data-policy="/privacy/" data-policy-version="2026-11-15" data-categories="analytics marketing"></script>
76
+ ```
77
+
78
+ **2. With a bundler** (Vite, webpack, esbuild…). Import it at the top of your entry file, after a small module that sets your config:
79
+
80
+ ```js
81
+ import './consent-config.js'; // window.MelioraConsentConfig = { categories: { analytics: {} }, policyUrl: '/privacy/' };
82
+ import 'meliora-consent'; // the stored choice and Consent Mode defaults apply at once
83
+ import 'meliora-consent/css';
84
+ ```
85
+
86
+ A bundle that loads as a module or with `defer` runs after the page is parsed. That's fine when your Google tags are tagged with `data-consent` (basic mode, recommended). If they load untagged, use the script tag instead, so the Consent Mode defaults come first.
87
+
88
+ **3. From a CDN, without installing.** npm packages are on jsDelivr automatically:
89
+
90
+ ```html
91
+ <script src="https://cdn.jsdelivr.net/npm/meliora-consent@0.2.0/dist/meliora-consent.min.js"
92
+ data-policy="/privacy/" data-policy-version="2026-11-15" data-categories="analytics marketing"></script>
93
+ ```
94
+
95
+ The [demo page](https://melioradev.github.io/meliora-consent/#install-title) builds this line for you. Then tag your tracking scripts with their category (below), and add a **Cookie settings** button to your footer:
96
+
97
+ ```html
98
+ <button type="button" data-consent-open hidden>Cookie settings</button>
99
+ ```
100
+
101
+ It starts hidden and appears only when there are choices to make, so it never shows without JavaScript. If you use [Meliora Accessibility](https://github.com/melioradev/meliora-accessibility) 1.1.0 or later, its menu also offers "Cookie settings".
102
+
103
+ | Attribute | What it sets |
104
+ |---|---|
105
+ | `data-categories` | The categories your site uses: `functional`, `analytics`, `marketing` (separated by spaces). |
106
+ | `data-policy` | Link to your cookie or privacy policy. |
107
+ | `data-policy-he`, `data-policy-ar`, … | The policy in another language. |
108
+ | `data-policy-version` | Change it whenever your cookies or wording change: everyone is asked again. |
109
+ | `data-consent-mode="false"` | You don't use Google tags: skip Consent Mode. |
110
+ | `data-gpc="false"` | Don't apply Global Privacy Control. |
111
+ | `data-renew-days` | Ask again after this many days (default 365). |
112
+ | `data-cookie-domain` | Share the choice with subdomains, e.g. `.example.com`. |
113
+ | `data-log` | URL that receives each choice as proof of consent ([example Worker](examples/consent-log-worker.mjs)). |
114
+ | `data-insert-after` | CSS selector to place the banner after in the DOM. |
115
+ | `data-css="none"` | Don't load the stylesheet; include `dist/meliora-consent.css` yourself. |
116
+
117
+ To list each cookie in the settings dialog, add `window.MelioraConsentConfig` (see [Configuration](#configuration)) in a file loaded just before the script. It wins over the attributes.
118
+
119
+ With a strict Content-Security-Policy, the script tag and bundler routes need no extra rules; the CDN route needs `https://cdn.jsdelivr.net` in `script-src` and `style-src`. The separate files in `src/` (as in 0.1.0) still work, and are in the npm package too.
120
+
121
+ ### Tagging scripts
122
+
123
+ - `type="text/plain"` makes the browser ignore the script. `data-consent` names its category: `functional`, `analytics` or `marketing` (several, separated by spaces, means all are needed).
124
+ - When the category is allowed, the script is re-created as a real script, in document order. Scripts without `async` keep their order. Use `data-type="module"` for module scripts.
125
+ - Prefer external files. Inline tagged scripts only run if your Content-Security-Policy allows them (a nonce is copied over).
126
+ - Add each vendor's domains to your CSP as usual.
127
+
128
+ ### Embeds
129
+
130
+ ```html
131
+ <iframe data-consent="marketing" data-src="https://www.youtube.com/embed/…" data-provider="YouTube"
132
+ title="Product demo video" width="560" height="315"></iframe>
133
+ ```
134
+
135
+ Until the category is allowed, a placeholder takes the iframe's place: "This content is from YouTube. Showing it allows marketing cookies.", with "Allow marketing cookies and show" and "Cookie settings" buttons.
136
+
137
+ ## Configuration
138
+
139
+ For cookie lists and everything else, put this in its own file (so your CSP can stay strict) and load it just before the banner's script:
140
+
141
+ ```js
142
+ window.MelioraConsentConfig = {
143
+ policyUrl: {default: '/privacy/', he: '/he/privacy/'}, // or one URL
144
+ policyVersion: '2026-11-15', // change it when the cookie list or wording changes: everyone is asked again
145
+ necessary: {cookies: [/* listed in the dialog, never blocked */]},
146
+ categories: { // only the categories you list appear
147
+ analytics: {
148
+ cookies: [
149
+ {name: '_ga', provider: 'Google Analytics', purpose: {default: 'Tells visitors apart', he: '…'}, duration: {default: '2 years', he: '…'}},
150
+ {name: '_ga_*', provider: 'Google Analytics', purpose: {default: 'Keeps the session state'}, duration: {default: '2 years'}},
151
+ ],
152
+ storageKeys: [], // localStorage keys to delete when the category is withdrawn
153
+ },
154
+ // functional: {...}, marketing: {...}
155
+ },
156
+ googleConsentMode: true, // false if you don't use Google tags
157
+ adsDataRedaction: true,
158
+ honorGPC: true,
159
+ renewAfterDays: 365, // 180 for French audiences (CNIL)
160
+ cookieDomain: null, // e.g. '.example.com' to share the choice with subdomains
161
+ logEndpoint: null, // e.g. '/api/consent': see examples/consent-log-worker.mjs
162
+ insertAfter: null, // CSS selector; default: after the accessibility button, or the skip link
163
+ text: {}, // string overrides per language, e.g. {he: {title: '…'}, default: {…}}
164
+ };
165
+ ```
166
+
167
+ Cookie names ending in `*` match by prefix. List every cookie your site sets: this repository's tests fail on undeclared cookies, and yours should too.
168
+
169
+ ### Theming
170
+
171
+ The banner uses your site's CSS custom properties when they exist, and light defaults otherwise. They're the same ones Meliora Accessibility uses:
172
+
173
+ `--ink` (text) · `--muted` · `--surface` (background) · `--tint` (hover) · `--line` (borders) · `--field-border` · `--brand` (links) · `--button` / `--button-hover` · `--focus`
174
+
175
+ Define them for your dark theme and the banner follows.
176
+
177
+ ## Google Consent Mode v2
178
+
179
+ The boot script calls `gtag('consent', 'default', …)` with everything denied (except `security_storage`) before any Google tag, then `gtag('consent', 'update', …)` with the stored choice. After each choice it sends an update and pushes `{event: 'meliora_consent_update', meliora_consent: {…}}` to the `dataLayer`.
180
+
181
+ | Category | Consent Mode signals |
182
+ |---|---|
183
+ | functional | `functionality_storage`, `personalization_storage` |
184
+ | analytics | `analytics_storage` |
185
+ | marketing | `ad_storage`, `ad_user_data`, `ad_personalization` |
186
+
187
+ - **Basic mode** (recommended): tag Google's scripts with `data-consent`. Nothing reaches Google before a choice.
188
+ - **Advanced mode:** leave Google's scripts untagged. They load at once and send cookieless pings while consent is denied. Whether that's acceptable is a legal decision for the site owner.
189
+ - With Google Tag Manager, put the boot script before the container snippet.
190
+
191
+ ## JavaScript API
192
+
193
+ ```js
194
+ MelioraConsent.get(); // {functional, analytics, marketing}, or null if undecided
195
+ MelioraConsent.has('analytics'); // true or false
196
+ MelioraConsent.open(); // opens the settings dialog
197
+ MelioraConsent.reset(); // forgets the choice, deletes declared cookies, reloads
198
+ document.addEventListener('meliora-consent', e => { /* e.detail: {choices, method, gpc} */ });
199
+ ```
200
+
201
+ `<html data-consent="analytics marketing">` lists the allowed categories, for CSS.
202
+
203
+ ## How the choice is stored
204
+
205
+ `localStorage` key `meliora.consent`:
206
+
207
+ ```json
208
+ {"v":1,"id":"5f0c…","policy":"2026-11-15","ts":"2026-11-15T12:00:00.000Z","method":"custom","gpc":false,
209
+ "choices":{"functional":false,"analytics":true,"marketing":false}}
210
+ ```
211
+
212
+ The visitor is asked again when `policyVersion` changes, a category is added, or the choice is older than `renewAfterDays`. If storage is blocked, the choice lasts for the current page only. The optional consent log stores the same record without IP address or user agent ([example Worker](examples/consent-log-worker.mjs)).
213
+
214
+ ## With Meliora Accessibility
215
+
216
+ - The accessibility button is bottom-right; the banner is a card at the bottom-left. When there isn't room for both, the banner becomes a full-width sheet and sets `--meliora-consent-inset`, so the accessibility button moves above it (Meliora Accessibility 1.1.0 or later).
217
+ - Tab order: skip link → accessibility button → its × → banner.
218
+ - High contrast, Stop animations and text size from the accessibility menu apply to the banner and the dialog.
219
+
220
+ ## Accessibility
221
+
222
+ - The banner is a named region early in the reading order. It isn't focused or announced on load, so it doesn't interrupt. After a choice, "Cookie choices saved" is announced and focus moves to `<main>`.
223
+ - The dialog starts on its title, Esc closes it without saving, and focus returns to the button that opened it.
224
+ - Switches are `<button aria-pressed>`, and the knob position, not only its color, shows the state.
225
+ - Focus is never hidden behind the banner (WCAG 2.2, 2.4.11), and a spacer at the end of the page lets the last content scroll above it.
226
+ - Every button is at least 44×44 px, and the banner is never more than half the screen high.
227
+
228
+ Tested with axe, keyboard-only use, 200% text at 320px, and right-to-left languages. Please report anything that doesn't work with your browser or assistive technology in [Issues](https://github.com/melioradev/meliora-consent/issues).
229
+
230
+ A cookie banner doesn't make a site compliant on its own, and nothing here is legal advice. Check your cookie list and wording with someone who knows the law where your visitors are.
231
+
232
+ ## Development
233
+
234
+ ```sh
235
+ npm install
236
+ npm run serve # demo at http://127.0.0.1:8790/
237
+ npm run build # regenerate dist/ from src/ (no dependencies)
238
+ npm run check # dist/ up to date, syntax check
239
+ npx playwright test # browser tests: Chromium, Firefox, WebKit
240
+ ```
241
+
242
+ The tests serve every page with a strict Content-Security-Policy and fail on any CSP violation or page error. The tests that run with the accessibility menu need a checkout of [meliora-accessibility](https://github.com/melioradev/meliora-accessibility) next to this repository, and are skipped otherwise.
243
+
244
+ Not included: IAB TCF, regional variants by geolocation, automatic blocking of untagged scripts, a hosted cookie scanner, and US "Do Not Sell or Share" flows beyond honoring GPC.
245
+
246
+ ## License
247
+
248
+ **MIT with visible attribution:** see [LICENSE](LICENSE).
249
+
250
+ - You may use, copy, modify and redistribute it, including in commercial products, for free.
251
+ - Every site or product that uses it, or a modified version, must visibly show **"Made by Meliora Novotech Ltd"** (or a translation), with working links to <https://melioranovotech.com> and to this repository.
252
+ - The unmodified banner already shows this credit at the bottom of its cookie settings dialog, so keeping it there is all you need to do.
253
+
254
+ ---
255
+
256
+ <p align="center">Made by <a href="https://melioranovotech.com">Meliora Novotech Ltd</a></p>
@@ -0,0 +1,129 @@
1
+ /*! Meliora Consent — https://github.com/melioradev/meliora-consent
2
+ Made by Meliora Novotech Ltd — MIT with visible attribution, see LICENSE. */
3
+ /* Banner, settings dialog, embed placeholder and status message.
4
+ Colors follow your site's CSS custom properties when they exist (--ink, --surface, --line, --button, --focus…),
5
+ the same ones the Meliora accessibility panel uses, with built-in light defaults otherwise. */
6
+
7
+ .mc {
8
+ --mc-bg: var(--surface, #fff); --mc-fg: var(--ink, #15173f); --mc-muted: var(--muted, #4a4f68);
9
+ --mc-line: var(--line, #d8dfeb); --mc-border: var(--field-border, #8d95ad); --mc-tint: var(--tint, #eaf1fb);
10
+ --mc-button: var(--button, #2557b4); --mc-button-hover: var(--button-hover, #1c4592); --mc-button-ink: #fff;
11
+ --mc-link: var(--brand, #2557b4); --mc-focus: var(--focus, #2557b4);
12
+ box-sizing: border-box; color: var(--mc-fg); font-size: 1rem; line-height: 1.5; font-weight: 400; text-align: start;
13
+ }
14
+ .mc *, .mc *::before, .mc *::after { box-sizing: border-box; }
15
+ :where(.mc) p { margin: 0; }
16
+ .mc a { color: var(--mc-link); text-decoration: underline; text-underline-offset: .15em; }
17
+ .mc :focus-visible { outline: 3px solid var(--mc-focus); outline-offset: 2px; }
18
+ .mc-title { margin: 0 0 .35rem; color: var(--mc-fg); font-size: 1.125rem; font-weight: 650; line-height: 1.3; letter-spacing: 0; text-transform: none; }
19
+ /* Space at the end of the page while the banner shows, so the last content can scroll above it. */
20
+ .mc-spacer { display: block; margin: 0; padding: 0; pointer-events: none; }
21
+
22
+ /* ---------- buttons: Accept, Reject and Choose share one style (no button is favoured) ---------- */
23
+ .mc-buttons { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; }
24
+ .mc-btn {
25
+ flex: 1 1 auto; min-width: 2.75rem; min-height: 2.75rem; margin: 0; padding: .5rem 1rem;
26
+ border: 2px solid var(--mc-button); border-radius: .5rem; background: var(--mc-button); color: var(--mc-button-ink);
27
+ font: inherit; font-weight: 600; line-height: 1.25; cursor: pointer;
28
+ }
29
+ .mc-btn:hover { border-color: var(--mc-button-hover); background: var(--mc-button-hover); }
30
+
31
+ /* ---------- banner (first layer) ---------- */
32
+ /* Card at the bottom-left (physical left in LTR and RTL: the accessibility button owns the bottom-right). */
33
+ .mc-banner {
34
+ position: fixed; z-index: 59; left: 1rem; bottom: 1rem; width: min(26rem, calc(100% - 2rem));
35
+ max-height: 50dvh; overflow: auto; overscroll-behavior: contain; margin: 0; /* sites often style <section> */
36
+ padding: 1rem 1.25rem 1.25rem; border: 1px solid var(--mc-border); border-radius: 1rem;
37
+ background: var(--mc-bg); box-shadow: 0 .5rem 1.5rem -.5rem rgb(17 19 58 / .35);
38
+ }
39
+ /* Not enough room beside the button: a full-width sheet; the script lifts the button with --meliora-consent-inset. */
40
+ .mc-banner.mc-sheet { left: 0; right: 0; bottom: 0; width: auto; border-width: 1px 0 0; border-radius: 1rem 1rem 0 0; }
41
+ .mc-text { margin-top: .25rem; }
42
+ .mc-gpc { margin-top: .75rem; padding: .5rem .75rem; border-inline-start: 3px solid var(--mc-button); background: var(--mc-tint); font-size: .9375rem; }
43
+
44
+ /* ---------- status message (role="status"), shown for 2.5 s where the banner was ---------- */
45
+ .mc-status {
46
+ position: fixed; z-index: 59; left: 1rem; bottom: 1rem; max-width: min(20rem, calc(100% - 6rem)); margin: 0;
47
+ padding: .5rem .85rem; border: 1px solid #fff; border-radius: .5rem; background: #15173f; color: #fff; font-weight: 600;
48
+ }
49
+ .mc-status:empty { padding: 0; border: 0; }
50
+
51
+ /* ---------- settings dialog (second layer) ---------- */
52
+ dialog.mc-dialog {
53
+ width: min(36rem, calc(100% - 2rem)); max-width: none; max-height: calc(100dvh - 2rem); margin: auto; padding: 0;
54
+ overflow: auto; overscroll-behavior: contain;
55
+ border: 1px solid var(--mc-border); border-radius: 1rem; background: var(--mc-bg); color: var(--mc-fg);
56
+ }
57
+ dialog.mc-dialog::backdrop { background: rgb(10 12 40 / .45); }
58
+ @media (max-width: 30em) {
59
+ dialog.mc-dialog { width: 100%; max-height: 88dvh; margin: auto 0 0; border-width: 1px 0 0; border-radius: 1rem 1rem 0 0; }
60
+ }
61
+ .mc-head {
62
+ position: sticky; top: 0; z-index: 1; display: flex; align-items: center; justify-content: space-between; gap: .5rem;
63
+ padding: .75rem .75rem .75rem 1.25rem; border-bottom: 1px solid var(--mc-line); background: var(--mc-bg);
64
+ }
65
+ .mc-dialog[dir="rtl"] .mc-head { padding: .75rem 1.25rem .75rem .75rem; }
66
+ .mc-head .mc-title { margin: 0; font-size: 1.2rem; }
67
+ .mc-head .mc-title:focus { outline: none; }
68
+ .mc-head .mc-title:focus-visible { outline: 3px solid var(--mc-focus); }
69
+ .mc-close {
70
+ display: grid; place-items: center; flex-shrink: 0; width: 2.75rem; height: 2.75rem; padding: 0;
71
+ border: 0; border-radius: 50%; background: none; color: inherit; cursor: pointer;
72
+ }
73
+ .mc-close:hover { background: var(--mc-tint); }
74
+ .mc-close svg { width: 1.25rem; height: 1.25rem; }
75
+ .mc-body { padding: 1rem 1.25rem 1.25rem; }
76
+ .mc-body > .mc-gpc { margin: 0 0 .75rem; }
77
+
78
+ .mc-cats { margin: 0; padding: 0; list-style: none; }
79
+ .mc-cat { padding: .75rem 0; border-bottom: 1px solid var(--mc-line); }
80
+ .mc-cat:first-child { padding-top: 0; }
81
+ .mc-row {
82
+ display: flex; align-items: center; justify-content: space-between; gap: 1rem; width: 100%; min-height: 2.75rem;
83
+ margin: 0; padding: .25rem .5rem; border: 0; border-radius: .5rem; background: none; color: inherit; font: inherit; text-align: start;
84
+ }
85
+ button.mc-row { cursor: pointer; }
86
+ button.mc-row:hover { background: var(--mc-tint); }
87
+ .mc-label { font-weight: 600; }
88
+ .mc-always { color: var(--mc-muted); font-size: .9375rem; font-weight: 600; }
89
+ .mc-desc { padding: 0 .5rem; color: var(--mc-muted); font-size: .9375rem; }
90
+
91
+ /* Switch: the knob position (not only its color) shows the state. Same as the accessibility panel. */
92
+ .mc-switch { position: relative; flex-shrink: 0; width: 2.1rem; height: 1.2rem; border: 1px solid var(--mc-border); border-radius: 1rem; background: var(--mc-line); }
93
+ .mc-switch::after {
94
+ content: ""; position: absolute; top: 50%; left: .15rem; width: .8rem; height: .8rem; margin-top: -.4rem;
95
+ border-radius: 50%; background: var(--mc-muted); transition: left .15s;
96
+ }
97
+ [aria-pressed="true"] > .mc-switch { border-color: var(--mc-button); background: var(--mc-button); }
98
+ [aria-pressed="true"] > .mc-switch::after { left: calc(100% - .95rem); background: #fff; }
99
+ .mc[dir="rtl"] .mc-switch { transform: scaleX(-1); }
100
+
101
+ .mc-details { margin: .25rem .5rem 0; font-size: .9375rem; }
102
+ .mc-details summary { padding: .6rem 0; font-weight: 500; cursor: pointer; }
103
+ .mc-cookie { display: grid; gap: .25rem; margin: .25rem 0 .5rem; padding: .5rem .75rem; border: 1px solid var(--mc-line); border-radius: .5rem; }
104
+ .mc-cookie div { display: grid; grid-template-columns: minmax(5.5rem, auto) 1fr; gap: .75rem; }
105
+ .mc-cookie dt { font-weight: 600; }
106
+ .mc-cookie dd { margin: 0; overflow-wrap: anywhere; }
107
+ .mc-cookie code { font: .875rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
108
+ .mc-policy { margin-top: 1rem; }
109
+ .mc-credit { margin-top: .75rem; padding-top: .75rem; border-top: 1px solid var(--mc-line); color: var(--mc-muted); font-size: .8125rem; text-align: center; }
110
+
111
+ /* ---------- placeholder for an embed waiting for consent ---------- */
112
+ iframe[data-consent][hidden] { display: none !important; }
113
+ .mc-embed {
114
+ display: grid; align-content: center; gap: .5rem; width: 100%; padding: 1.25rem;
115
+ border: 1px solid var(--mc-border); border-radius: .75rem; background: var(--mc-tint);
116
+ }
117
+ .mc-embed-title { font-weight: 650; }
118
+ .mc-embed .mc-buttons { margin-top: .25rem; }
119
+
120
+ /* ---------- the accessibility panel's settings apply ---------- */
121
+ html.a11y-contrast .mc-banner, html.a11y-contrast dialog.mc-dialog, html.a11y-contrast .mc-embed { border: 2px solid #000; box-shadow: none; }
122
+ html.a11y-contrast .mc-switch { border: 2px solid #000; background: #fff; }
123
+ html.a11y-contrast .mc-switch::after { background: #000; }
124
+ html.a11y-contrast [aria-pressed="true"] > .mc-switch { background: #000; }
125
+ html.a11y-contrast [aria-pressed="true"] > .mc-switch::after { background: #fff; }
126
+ html.a11y-contrast .mc-status { background: #000; border: 2px solid #fff; }
127
+ html.a11y-contrast .mc-status:empty { border: 0; }
128
+ @media (prefers-reduced-motion: reduce) { .mc-switch::after { transition: none; } }
129
+ @media print { .mc-banner, .mc-status { display: none !important; } }