Codebase Review
Purpose. A snapshot of the LCARdS codebase as it stands today, intended as the planning input for follow-up PRs. Findings reference real files and line numbers; recommendations are sliced into reviewable PR-sized chunks.
Scope of this review. Source under
src/, documentation underdoc/, build/validation tooling underscripts/and.github/workflows/. The backend custom component (custom_components/lcards/) is out of scope.Compiled. April 2026, against
devat the time of the accompanying docs/refactor PR.
1. Executive Summary
LCARdS is a Lit-based suite of LCARS-themed custom Lovelace cards for Home Assistant, organised around a global singleton core (window.lcards.core.*) that owns themes, datasources, rules, animations, validation, presets, and asset loading. Cards are thin Lit web components that talk to that core; an editor framework on top of LCARdSBaseEditor exposes every card config in the Lovelace UI.
Strengths
- Clean architectural seams. The
BaseService→ core singleton →LCARdSCard→ card hierarchy is consistent and easy to reason about. HASS propagation, lifecycle hooks (_handleFirstUpdate,_handleHassUpdate,_renderCard,_onRulePatchesChanged), and cleanup are centralised onLCARdSCard, so individual cards stay small in surface area. - Documented internal contracts.
.github/copilot-instructions.mdand.github/instructions/*.mddescribe the singleton model, template evaluator, datasources, animation system, and editor patterns with concrete do/don't examples — unusually thorough for a HA custom card project. - Validator infrastructure already exists.
scripts/validate-css-vars.jsis a 4-pass governance gate on--lcars-*,--lcards-*, andtheme:paths, wired intonpm run buildand CI. This sets a precedent for the doc-example validator added in this PR. - Unified evaluator and resolver.
UnifiedTemplateEvaluator(JS / token / datasource / Jinja2) andThemeTokenResolver(withlighten/darken/alpha/basecomputed expressions) consolidate what would otherwise be scattered string-mangling logic. The "two-step pattern" for canvas/SVG colour resolution is documented and consistently used. - Comprehensive VitePress docs. 92 markdown pages spanning user-facing card reference, core feature docs, architecture, and API reference. A custom markdown rule wraps
{{...}}in<span v-pre>so HA template syntax renders cleanly.
Highest-Risk Tech Debt
- Two
deepMergeimplementations with different semantics.src/utils/deepMerge.jsMUTATES its target;src/core/config-manager/merge-helpers.jsreturns a new object. Both are used — sometimes within the same render path. Consolidating without regressions requires a behaviour-preserving migration with tests, which is why it is recommended as a P0 follow-up rather than collapsed in the current PR. - YAML examples in docs were never validated against code. This PR adds
npm run validate:doc-examplesand fixed real bugs along the way (type: custom:lcards-msd→lcards-msd-card,type: custom:lcards-button-card→lcards-button). 41 snippet-style blocks remain as parse warnings; bringing them to zero requires careful per-block decisions (split into multiple blocks vs. markyaml no-validate). - Large card files. Several cards exceed 4 000 LOC (
lcards-button.js6 847,lcards-slider.js4 150,lcards-elbow.js3 604,lcards-card.jsbase 4 381). Risk: lifecycle hooks, rendering, and zone math all live in the same file, which makes targeted review and unit testing harder. Splitting is a series of focused refactor PRs. - Editor styles partially shared.
editor-styles.js(320 lines) andeditor-component-styles.js(184 lines) provide canonical tokens, but ~10 editor components still declare their ownstatic get styles()without importing them. Drift is likely. - No automated test infrastructure. Validation happens at build time (CSS-vars, types, hassfest) and via manual smoke testing in HA. There is no Vitest/Jest harness; introducing one would unlock the deepMerge consolidation, schema drift checks, and template-evaluator regression tests.
- Pre-existing TypeScript errors.
npm run typecheckreports severalTS2339errors inpanels/lcards-config-panel.js(Lit types not pulled into@ts-checkJSDoc) andutils/state-color-resolver.js. These are not introduced by this PR but should be cleared before adding any stricter type gates.
2. Repository Map
lcards/
├── custom_components/lcards/ HA backend (Python integration; out of scope here)
├── doc/ VitePress source (92 md files)
│ ├── .vitepress/config.mts Nav + sidebar definitions
│ ├── architecture/ Internals & subsystem reference
│ ├── cards/ Per-card user docs
│ ├── configuration/ HA-side setup & integration
│ ├── core/ Per-card features (presets, rules, themes…)
│ ├── development/ Developer guides + API reference
│ ├── dev/ Internals (this report lives here)
│ └── getting-started/ Onboarding & migration
├── scripts/ Build-time tooling (CSS-var validator, version, doc-example validator)
├── src/
│ ├── api/ Public-surface helpers
│ ├── base/ LCARdSCard (4 381 LOC), LCARdSNativeCard, LCARdSActionHandler
│ ├── cards/ All Lovelace cards (button/elbow/slider/chart/data-grid/select-menu/msd/alert-overlay)
│ ├── charts/ ApexCharts adapter
│ ├── core/ Singleton subsystems
│ │ ├── animation/ Anime.js v4 wrapper, presets, registry, timeline resolver
│ │ ├── components/ Component registry (SVG/templates)
│ │ ├── config-manager/ Layered config merge, provenance, schema fan-out
│ │ ├── data-sources/ DS registry, processors, polling, history
│ │ ├── helpers/ Cross-cutting helpers
│ │ ├── packs/ Style/theme packs (textures, backgrounds, sounds)
│ │ ├── presets/ StylePresetManager + preset trees
│ │ ├── rules/ RulesEngine (conditional patches)
│ │ ├── screen-effects/ Full-screen effects pipeline
│ │ ├── services/ HA service wrappers
│ │ ├── sound/ Sound system
│ │ ├── systems-manager/ Overlay registry, lifecycle
│ │ ├── templates/ UnifiedTemplateEvaluator
│ │ ├── themes/ ThemeManager + ThemeTokenResolver
│ │ └── validation-service/ Schema registry + per-domain validators
│ ├── editor/
│ │ ├── base/ LCARdSBaseEditor, editor-styles.js, editor-component-styles.js
│ │ ├── cards/ One editor per card
│ │ ├── components/ Reusable tabs / pickers / list editors / yaml mode
│ │ ├── dialogs/ Modal dialogs
│ │ └── utils/ FormField, color helpers, schema-driven form
│ ├── msd/ MSD card pipeline (multi-overlay rendering)
│ ├── panels/ LCARdSConfigPanel (HA panel UI)
│ ├── strategies/ Lovelace strategies
│ ├── types/ TS type stubs
│ ├── utils/ Cross-cutting utilities (deepMerge, css-length, theme, fileutils, hashing…)
│ ├── lcards-vars.js Allowlist for `--lcards-*` CSS variables
│ └── lcards.js Entry point: registers cards, boots core
└── tools/ Dev helpers (e.g. `create-editor.js`)Mapping to the workstream lens
| Workstream area | Primary location |
|---|---|
| Core systems | src/core/** |
| Cards | src/cards/**, src/base/LCARdSCard.js, src/msd/** |
| Configuration panel | src/panels/lcards-config-panel.js, src/strategies/** |
| Editors | src/editor/** |
3. Findings
3.1 Structure & modularity
- Singleton boot order is implicit.
src/lcards.jsregisterscustomElements.define(...)for all cards inside a try block, thenwindow.customCards.push(...). The order of singleton creation lives insrc/core/lcards-core.js#_performInitialization. This works, but a diagram or sequence outline (Mermaid) indoc/architecture/systems-arch.mdwould shorten the onboarding curve. - Large card files conflate concerns. Button (6 847 LOC), Slider (4 150), Elbow (3 604) include preset resolution, zone math, SVG generation, action wiring, and rule integration in a single class. Candidate splits:
<card>-zones.js(zone math; already a defined contract on the base)<card>-svg.js(markup helpers)<card>-presets.js(preset normalisation)
LCARdSCardis the centre of gravity. 4 381 LOC. Many helpers (_resolveThemeToken,_processTextFields,_rebuildZones,_getMergedStyleWithRules,_pxToGridUnits) could move into focused mixins or helper modules without changing the public contract.- MSD coordinator vs. core.
src/msd/pipeline/MsdCardCoordinator.jsdirectly importsdeepMergefrom the config-manager helpers, while most cards import a differentdeepMergefromutils/deepMerge.js. See 3.2. - Editor base + tabs are well-factored.
LCARdSBaseEditor,FormField,editor-styles, andeditor-component-stylesalready encode "the canonical way" — the gap is enforcement, not design.
3.2 Duplication hotspots
| # | Pattern | Where | Notes |
|---|---|---|---|
| 1 | deepMerge (mutating) | src/utils/deepMerge.js:18 | Used by lcards-slider, lcards-button (immutable variant), lcards-chart, ApexChartsAdapter, lcards-slider-editor, core/animation/presets, core/animation/resolveTimelines, StylePresetManager. |
| 2 | deepMerge (immutable) | src/core/config-manager/merge-helpers.js:29 | Used by LCARdSCard, LCARdSBaseEditor, RulesEngine, MsdCardCoordinator, CoreConfigManager. Different semantics from #1. |
| 3 | clamp | inline in lcards-elbow.js:2545, core/animation/resolveAnimParams.js:138, packs/textures/effects/LevelTextureEffect.js:14, packs/backgrounds/BackgroundAnimationRenderer.js:244, plus linearMap.js accepts a clamp flag | Each is a different signature; canonicalising on (v, lo, hi) is safe. |
| 4 | isPlain / isPlainObject | utils/deepMerge.js:10, core/config-manager/merge-helpers.js:158 | Behaviourally equivalent for plain objects; differ on class instances. Pick one in a shared util. |
| 5 | Structural clone via JSON.parse(JSON.stringify(...)) | 59 occurrences across src/ | Loses Date/Map/undefined; risks silent bugs when configs grow. Replace with a single cloneConfig() helper that uses structuredClone where available. |
| 6 | getNestedValue / setNestedValue | core/config-manager/merge-helpers.js:174 exists; ad-hoc dot-path walks recur in RulesEngine, LCARdSBaseEditor, lcards-button, provenance-tracker | Move all dot-path access through the existing helpers. |
| 7 | Static static get styles() blocks that don't import editor-styles | 10 components in src/editor/components/** | Drift risk on focus rings, button styling, label spacing. |
| 8 | Per-card "preset normalisation" pipelines | lcards-button._resolvePreset, lcards-slider._resolvePreset, lcards-elbow._resolvePreset, lcards-select-menu._resolvePreset | All do: load preset → deepMerge user overrides → resolve theme tokens. Candidate for a BasePresetResolver. |
| 9 | Animation easing/duration default fallbacks | scattered in core/animation/resolveAnimParams.js, core/animation/presets.js, lcards-animation-editor.js, lcards-background-animation-editor.js | Defaults should live in one place, exported as a frozen constant. |
| 10 | setupActions(...) boilerplate | every card | Already centralised on LCARdSCard, but each card calls it identically with tap_action/hold_action/double_tap_action. A higher-order helper bindStandardActions(this, host) would remove ~5 LOC per card. |
3.3 CSS / styling consistency
- Canonical sources exist.
editor-styles.js(~320 lines) andeditor-component-styles.js(~184 lines) define typography, button, field, panel, and tab tokens.editorStylesis consumed by all cards' editors and ~half of the shared components. - Drift list (components missing the import).
lcards-chart-live-preview.js,lcards-msd-live-preview.js,lcards-shape-texture-editor.js,pack-explorer/lcards-pack-explorer-tab.js,templates/lcards-template-sandbox.js,editors/lcards-color-selector.js,editors/lcards-unified-segment-editor.js,editors/lcards-grid-row-editor.js,editors/lcards-border-editor.js,editors/lcards-slider-range-visualizer.js. - Config panel vs. editor parity.
lcards-config-panel.jsand the per-card editors share a similar tab/section layout (tab bar → panel → field group → row of fields), but they don't share CSS. Apanel-tab-styles.jsmodule re-using the editor tokens would eliminate duplicated padding/border definitions. --lcards-*allowlist is well-policed. The CSS-var validator prevents new variables from being added without an allowlist update. Keep this gate.
3.4 Testing gaps
- No unit tests at all. No
vitest,jest, ormochaindevDependencies. Build-time gates (CSS vars, hassfest, version) and manual smoke tests in HA are the only checks. - High-leverage candidates for a first test pass:
deepMerge(both variants) — pin current semantics before consolidation.ThemeTokenResolver— token resolution and computed expression evaluation (lighten/darken/alpha/base).UnifiedTemplateEvaluator— JS / token / datasource / Jinja2 phases.RulesEngine— patch ordering and merge semantics.core/config-manager— layer ordering and provenance tracking.validate-doc-examples.js— golden fixture for the doc validator.
- No CI gate for
npm run typecheck. Pre-existing errors would block enabling it; clean those up first.
3.5 Documentation gaps
Resolved in this PR:
- VitePress sidebar pointed
/architecture/internals/datasource-buffersat a file that lives at/architecture/animations/datasource-buffers(broken link, masked byignoreDeadLinks: true). doc/cards/msd/index.md,doc/architecture/msd/index.md, anddoc/architecture/animations/rule-based-animations.mdreferencedtype: custom:lcards-msd(the registered element islcards-msd-card).doc/cards/msd/line-overlay.mdreferencedtype: custom:lcards-button-card(correct value islcards-button).- A
Developer / Internalsnav section now exists, linking this report. - A repo-native
npm run validate:doc-examplescommand was added and is documented indoc/development/index.md.
Remaining gaps (recommendations below):
- No top-level "What can each card do?" feature matrix; users have to read each card page to discover capabilities.
- No "common patterns" cookbook (e.g. "alert-coloured button that blinks when a sensor is unavailable") — most knowledge lives in the examples folder.
doc/development/custom-card.mdis light on the new card recipe (CARD_TYPE, getStubConfig, registration insrc/lcards.js,_getCardSize).- 41 snippet-style YAML blocks (multi-key "alternative values" examples) parse as duplicate-key warnings under js-yaml. They aren't bugs in user-facing terms, but they aren't valid HA YAML either.
4. Prioritised Recommendations Backlog
| Priority | Area | Recommendation | Effort | Risk | Suggested PR Slice | Files / Paths |
|---|---|---|---|---|---|---|
| P0 | core | Consolidate deepMerge into one canonical, documented immutable variant; migrate all callers. Ship with a tiny test fixture pinning current semantics. | M | Med | "Unify deepMerge" — single PR with table of before/after call sites + golden test. | src/utils/deepMerge.js, src/core/config-manager/merge-helpers.js, every importer (see §3.2 row 1-2). |
| P0 | tooling | Introduce a unit-test runner (Vitest, no transpile config required for ESM). Seed with deepMerge, ThemeTokenResolver, UnifiedTemplateEvaluator. | M | Low | "Bootstrap Vitest" — config + 3 small specs only; no CI changes. | vitest.config.js, tests/, package.json. |
| P0 | docs | ✅ Done. Tagged all 41 snippet-style YAML blocks with ```yaml alternatives so the doc validator skips YAML parsing (but still checks type refs) for those blocks. Then enable --strict in CI. | S | Low | "Make doc YAML zero-warning" — tagged blocks with the new alternatives meta hint. | doc/core/colours.md, doc/core/text-fields.md, doc/core/rules/*, doc/cards/msd/line-overlay.md, doc/configuration/browser-mod.md. |
| P1 | core | Replace ad-hoc clamp and isPlainObject clones with shared utils. | S | Low | "Shared maths/object utils" — add src/utils/numberUtils.js, re-export isPlain. | §3.2 rows 3, 4. |
| P1 | core | Replace 59 JSON.parse(JSON.stringify(...)) clones with cloneConfig() (using structuredClone when available, fallback otherwise). | M | Med | "Introduce cloneConfig()" — incremental: add helper, replace high-traffic call sites first. | src/utils/cloneConfig.js, callers found via grep -rn "JSON.parse(JSON.stringify" src/. |
| P1 | cards | Extract preset-normalisation into a BasePresetResolver mixin/helper. | M | Med | "Preset resolver shared helper" — one card per PR (start with lcards-button). | src/cards/lcards-button.js, lcards-slider.js, lcards-elbow.js, lcards-select-menu.js. |
| P1 | editors | Make editorStyles import mandatory across all editor components; add an ESLint-style grep gate to validate-css-vars to catch drift. | S | Low | "Editor CSS consistency" — one-line imports + remove duplicated tokens. | The 10 components listed in §3.3. |
| P1 | docs | Add a "Cards feature matrix" page (rows = cards, columns = capabilities like presets / zones / actions / animations / rules). | S | Low | "Feature matrix" — single new page, link from /cards/. | doc/cards/feature-matrix.md, doc/.vitepress/config.mts. |
| P1 | tooling | Wire npm run validate:doc-examples into .github/workflows/docs.yml once warnings are zero. | XS | Low | "Gate docs build on YAML validator" — single workflow edit. | .github/workflows/docs.yml. |
| P2 | core | Split LCARdSCard (4 381 LOC) into focused mixins: zones, text fields, rules patching, theme token glue, action wiring. | L | High | "LCARdSCard mixin split" — one mixin per PR, behaviour-preserving. | src/base/LCARdSCard.js, new src/base/mixins/*.js. |
| P2 | cards | Split lcards-button.js into preset / zone / SVG / class modules. | L | High | "lcards-button modularisation" — multi-PR series, one extracted module each. | src/cards/lcards-button.js, new src/cards/button/*.js. |
| P2 | tooling | ✅ Done. Imported all 7 non-MSD card schemas directly into scripts/validate-doc-examples.js (Node-importable ESM). Pre-processes each schema with stripSchemaRestrictions to remove empty enum: [], dangling $ref, and additionalProperties: false before compiling with ajv@8. Validates every card config node found in parsed YAML blocks (including nested cards). Schema issues reported in a separate ⚠️ schema warning(s) section; --strict promotes them to errors. | src/cards/schemas/*.js, scripts/validate-doc-examples.js. | |||
| P2 | tooling | Clear the 9 pre-existing TypeScript errors and gate npm run typecheck in CI. | M | Med | "Typecheck clean + CI gate." | src/panels/lcards-config-panel.js, src/utils/state-color-resolver.js. |
| P2 | config | Share styling between the HA LCARdSConfigPanel tabs and the per-card editor tabs. | M | Low | "Panel/editor CSS parity" — one new shared module. | src/panels/lcards-config-panel.js, src/editor/base/editor-styles.js. |
| P2 | docs | Cookbook of recipes (alert-coloured button, datasource-driven sparkline, rule-driven flash, theme override). | M | Low | "Cookbook section" — one or two recipes per PR. | doc/cards/cookbook/**, sidebar. |
5. Proposed Follow-up PR Plan
Sized for review-in-an-hour. Each builds on the previous but is standalone if the order shifts.
PR 1 — Bootstrap Vitest (P0)
- Goal. Make tests possible.
- Scope. Add Vitest config,
tests/utils/deepMerge.spec.jspinning both current implementations' semantics, README note. - Key files.
vitest.config.js,tests/utils/deepMerge.spec.js,package.json(addtestscript). - Validation.
npm testgreen, no production code touched.
PR 2 — Unify deepMerge (P0, depends on PR 1)
- Goal. One canonical, documented immutable
deepMerge; mutating call sites either migrate to immutable or use a clearly-namedmergeIntohelper for the rare case where mutation is the contract. - Scope. Move canonical impl into
src/utils/deepMerge.js, re-export fromsrc/core/config-manager/merge-helpers.jsfor back-compat, migrate all importers. - Key files. §3.2 rows 1-2.
- Validation. Vitest specs from PR 1, build, manual smoke test of preset inheritance and rules patching.
PR 3 — Doc YAML zero-warning + CI gate (P0/P1)
- Goal.
npm run validate:doc-examples --strictexits clean and is enforced bydocs.yml. - Scope. ✅ All 41 snippet-style blocks tagged with
```yaml alternatives(parse skipped; type refs still checked). The next step is to add the--strictflag and the workflow step. - Key files. §3.5 list,
.github/workflows/docs.yml. - Validation.
npm run validate:doc-examples:strictexits 0;npm run docs:buildsucceeds.
PR 4 — Shared maths/object utils (P1)
- Goal. Eliminate
clampandisPlainObjectduplicates. - Scope. New
src/utils/numberUtils.jswithclamp. PromoteisPlainto the canonical predicate. - Validation. Vitest spec for
clampedge cases (NaN, equal bounds, reversed bounds).
PR 5 — cloneConfig() (P1, depends on PR 1)
- Goal. One safe deep-clone helper; remove
JSON.parse(JSON.stringify(...)). - Scope. Add helper, replace top 10 highest-traffic call sites (rules engine, presets, theme manager, datasource subscribers).
- Validation. Spec covering Date/Map/undefined handling.
PR 6 — Editor CSS consistency (P1)
- Goal. Every editor component imports
editorStyles. Add a small grep validator to catch regressions. - Scope. §3.3 drift list; add a check to
validate-css-vars.js(or a sibling script) that fails if an.editor*.jsfile declaresstatic get styles()without importingeditorStyles. - Validation. Validator green; visual smoke test of each editor.
PR 7 — Cards feature matrix + cookbook seed (P1)
- Goal. A single page that answers "what can each card do?" and one worked recipe.
- Scope. New md pages, sidebar entries, cross-links from card pages.
- Validation.
npm run docs:build,npm run validate:doc-examples.
PR 8 — BasePresetResolver for buttons (P1)
- Goal. Extract preset normalisation from
lcards-button.jsinto a reusable helper; prove the shape on one card before generalising. - Scope. New
src/cards/preset/BasePresetResolver.js; refactor button only. - Validation. Visual smoke + Vitest spec for preset merge order.
PR 9 — Apply BasePresetResolver to slider/elbow/select-menu (P1)
- Goal. Roll out the helper from PR 8.
- Scope. One card at a time inside this PR if diff is small.
PR 10 — Typecheck clean + CI gate (P2)
- Goal.
npm run typecheckis clean and enforced. - Scope. Fix the 9 pre-existing errors, add the workflow step.
6. How These Conclusions Were Validated
Commands run
# Repo overview
git log --oneline origin/dev..HEAD
git diff --stat origin/dev..HEAD
find src -type f -name '*.js' | wc -l # 305
wc -l src/cards/*.js src/base/*.js # see §1, §3.1
# Build-time validators
npm run validate:css-vars # ✓ 305 files, all valid
npm run validate:doc-examples # ✓ added in this PR
npm run typecheck # 9 pre-existing errors
# Targeted searches
grep -rn 'function deepMerge\|const deepMerge\|deepMerge =' src/ --include='*.js'
grep -rn 'from.*merge-helpers\|from.*deepMerge' src/ --include='*.js'
grep -rn 'function clamp\|const clamp =' src/ --include='*.js'
grep -rn 'function isPlain\|function isPlainObject' src/ --include='*.js'
grep -rn 'JSON.parse(JSON.stringify' src/ --include='*.js' | wc -l # 59
grep -roh 'type: custom:lcards-[a-z-]*' doc/ --include='*.md' | sort -u
grep -rn 'static CARD_TYPE' src/cards/ --include='*.js'
grep -rL 'editorStyles\|editor-styles\|editor-component-styles' src/editor/components/ --include='*.js' \
| xargs grep -l 'static get styles\|static styles'What was specifically searched for
deepMergeduplication. Confirmed two implementations with different mutation semantics, in active use across 14+ files (§3.2 row 1-2).- CSS-token reuse.
editorStylesimport audit acrosssrc/editor/components/**(§3.3). - Schema validators in production code.
src/core/validation-service/index.js,SchemaRegistry.js,OverlayValidator.js,ValueValidator.js, plus the schemas insrc/core/validation-service/schemas/. Confirmed schemas are registered at runtime by cards (registerSchema()static methods), which is why a Node-side import of the production validator is non-trivial — recorded as P2 follow-up. - Doc/code drift. Cross-referenced
customElements.define(...)insrc/lcards.jsagainst everytype: custom:lcards-*value found indoc/**.md. Discrepancies fixed in this PR. - Sidebar reachability. Cross-referenced every
*.mdunderdoc/against the link list indoc/.vitepress/config.mts; found one broken sidebar link (/architecture/internals/datasource-buffers), fixed in this PR.
Limits of this review
- No runtime profiling. Statements about "high-traffic" call sites rely on import graph and code-reading, not measured frame budgets.
- No deep dive into MSD pipeline.
src/msd/is large and was treated as a black box for the purposes of this review; targeted refactor recommendations there are best made by an MSD-area owner. - The doc validator now performs schema validation. It checks unknown card types, parse failures, and validates parsed card configs against the real JSON Schemas for 7 card types (lcards-msd-card excluded). Schema issues are reported as warnings by default and promoted to errors with
--strict. The 10 current schema warnings reflect minor gaps between the schemas and the doc examples (e.g.backgroundobject form not in schema,layersas array vs object).
This document is intended to evolve. PR authors should update entries here as items are completed and add new findings as they surface.