29 KiB
A1–A8 Interactive Architecture Viewpoints
Status: design contract, version 0.2, 2026-07-18.
This document defines how STEM cards describe architecture, code and runtime evidence. It is a house convention, not a new UML standard. The convention is organized in the spirit of ISO/IEC/IEEE 42010:2022, uses UML 2.5.1 notation, adds selected MARTE 1.3 concepts for real-time systems, and uses PlantUML as the textual source of rendered diagrams.
Scope and rollout
A1–A8 is runtime-independent. The fixed viewpoints are intended to support embedded RTOS software, ordinary C++ object models, POSIX or standard threads, and asynchronous Boost.Asio applications without changing the navigation grammar.
The rollout is deliberately staged:
- current application: FreeRTOS C++ cards on RV32I/Hazard3;
- later application:
std::thread/std::jthreadand Boost.Asio; - not yet scheduled: concrete Asio/thread card repositories and lesson numbering.
The later profile is specified below so current diagrams do not bake in RTOS- only assumptions. It is a compatibility target, not a claim that the current FreeRTOS card already implements or teaches Asio.
Standards boundary
- ISO/IEC/IEEE 42010:2022 supplies the concepts of architecture descriptions, viewpoints, views and model kinds. A1–A8 is our concrete set of viewpoints. We do not claim full ISO conformance merely because a card uses these labels.
- UML 2.5.1 supplies the notation for classes, objects, components, interactions, activities and state machines. It does not prescribe A1–A8.
- MARTE 1.3 may annotate schedulable resources, time, hardware and allocation in RTOS cards. A MARTE property must come from configuration, code or measurement; authors must not invent period, deadline or execution time merely to fill a box.
- arc42 is a useful wider documentation template. A1–A8 is the smaller, interactive teaching slice used inside a card, not a replacement for an architecture description of the whole product.
- PlantUML is the text-first renderer. The
.pumlsource, not a manually edited SVG, is authoritative.
Primary references:
- https://www.iso.org/standard/74393.html
- https://www.omg.org/spec/UML/2.5.1
- https://www.omg.org/spec/MARTE/1.3
- https://arc42.org/overview
- https://plantuml.com/class-diagram
- https://plantuml.com/sequence-diagram
- https://plantuml.com/ascii-art
The fixed navigation model
The tab names remain stable across cards. A subtitle identifies the actual mechanism being taught.
A1 CONTEXT
A2 STRUCTURE
A3 DISPATCH
A4 APPLICATION
A5 FLOW
A6 STATE
A7 RUNTIME
A8 PATTERNS
These eight tabs ask eight different questions:
| ID | Question | Typical model kind |
|---|---|---|
| A1 CONTEXT | Where is the subject and what lies outside its boundary? | component, package or deployment view |
| A2 STRUCTURE | What static elements and relationships construct it? | class, component or object structure |
| A3 DISPATCH | How is an abstract call bound to the actual implementation? | class plus sequence/call-path view |
| A4 APPLICATION | How does application code specialize or compose it? | application class/object view |
| A5 FLOW | What happens in what order? | sequence or activity view |
| A6 STATE | Which lifecycles and state transitions exist? | state machine, never an unlabeled flowchart |
| A7 RUNTIME | What actually exists in memory and in the debugger now? | object diagram plus evidence overlay |
| A8 PATTERNS | Which design roles and trade-offs explain the construction? | participant map plus decision record |
Not every card needs all eight views. The top-level navigation keeps all eight positions stable; an omitted view is visibly unavailable and states why. A small card should not create decorative diagrams to fill empty tabs.
Viewpoint contracts
A1 CONTEXT
Purpose: establish system boundary and dependency direction before class details appear.
Required content:
- subject under study;
- upstream application actor or component;
- wrapped platform/API;
- runtime platform, hardware or simulator when relevant;
- one verb on every boundary-crossing relation.
For an RTOS wrapper this normally reads:
Application task
│ specializes / uses
▼
C++ wrapper
│ calls
▼
FreeRTOS C API
│ schedules on
▼
Hazard3 simulator
A1 must not list every method. It is a map, not a class reference.
A2 STRUCTURE
Purpose: show the static construction that the compiler sees.
Required content:
- class/interface/component names;
- relevant fields and operations only;
- visibility where it is part of the lesson;
- precise relationship type: generalization, realization, composition, aggregation or dependency;
- storage/ownership role when the structure owns a resource.
Class rows receive semantic anchors only when a card can connect them to a
source line or a compile-time proof. Inheritance, friend, templates and
visibility are CODE facts; they are never presented as runtime events.
A3 DISPATCH
Purpose: explain the binding mechanism between the generic boundary and the concrete implementation.
The title is fixed; the subtitle is mechanism-specific. Examples:
A3 DISPATCH · CRTP static dispatch
A3 DISPATCH · virtual dispatch through vtable
A3 DISPATCH · C callback and function pointer
A3 DISPATCH · event to subscriber
A3 DISPATCH · executor and completion handler
The diagram must identify:
- the call origin;
- the erased or abstract boundary;
- the cast, lookup or binding operation;
- the concrete target;
- whether the decision is compile-time or runtime;
- the ABI proof that distinguishes the mechanism.
This viewpoint prevents a wrapper from being mislabeled. A CRTP wrapper must
show static_cast<Derived*> and the absence of virtual dispatch. A dynamic
wrapper must show object address, vptr, vtable entry and indirect call.
A4 APPLICATION
Purpose: show one concrete use without repeating the complete library.
The generic mechanism is collapsed to its public contract. The view maps every
pattern or framework role to the names used in the exercise, for example
Strategy -> PaymentStrategy and Context -> ShoppingCart.
At least one application invariant is printed next to the model. Example:
"ShoppingCart depends on the strategy interface, not a payment variant."
A5 FLOW
Purpose: show causal order for one named scenario.
Rules:
- state the precondition and observable postcondition;
- use sequence diagrams for collaborating participants and activity diagrams for algorithmic branching;
- number messages in the same order as interactive steps;
- show returns only when they carry teaching information;
- split construction, scheduling and callback dispatch into phases when one page would hide the boundary.
A5 is the primary home of RUN steps and deterministic checkpoints.
A6 STATE
Purpose: separate lifecycle from execution flow.
When two state domains exist, draw two explicit lanes rather than merging them. For a FreeRTOS C++ task these are commonly:
C++ object lifetime: absent -> constructed -> alive -> destroyed
Kernel task state: Ready <-> Running <-> Blocked; Suspended; Deleted
Every transition has a trigger, guard or event. Values returned by platform APIs are platform states, not invented wrapper states. A wrapper-specific state machine is allowed only when the code really stores and enforces it.
A7 RUNTIME
Purpose: connect the model to a reproducible observation.
A7 is deliberately a hybrid, not pure UML. It combines an object diagram with selected debugger facts:
- object and subobject addresses;
this, callback context and handles;- fields, ownership pointers, TCB and stack interval;
- PC, SP and relevant argument registers;
- vptr/vtable target for dynamic dispatch;
- symbol/absence-of-symbol proof for static dispatch;
- timestamp, target, ELF revision and checkpoint identity.
Every value is either captured from a deterministic run or marked as a symbolic example. A screenshot without target, source revision and timestamp is illustration, not evidence.
A8 PATTERNS
Purpose: explain why the design has its shape and what it costs.
Each claimed pattern records:
- intent and problem;
- forces/constraints;
- canonical participant roles;
- exact mapping from role to project class/function;
- collaboration that makes the pattern real;
- invariant or test;
- benefit and cost;
- rejected alternative;
- conditions in which the pattern should not be used.
Pattern names are not decorative stereotypes. Facade is valid only when a
simpler boundary to a subsystem can be identified. RAII is valid only when
acquisition and release are safely paired for every legal lifetime. A callback
bridge may be an Adapter without making the whole wrapper an Adapter.
Authored viewpoint record
The future machine-readable form should preserve this conceptual record. This
is a design target, not yet a required card_source.json schema extension:
{
"id": "a3-dispatch",
"number": "A3",
"label": "DISPATCH",
"subtitle": "Dynamic polymorphism through vtable",
"purpose": "Trace the C callback to the concrete C++ override.",
"stakeholders": ["student", "teacher", "embedded developer"],
"concerns": ["C-to-C++ bridge", "void* context", "vtable", "ABI"],
"model_kind": "uml-sequence",
"source": "doc/assets/a3-dispatch.puml",
"fallback_text": "doc/assets/a3-dispatch.txt",
"spread_group": "task-dispatch",
"steps": [
{
"id": "dispatch-vptr",
"number": 4,
"mode": "RUN",
"svg_target": "dispatch-vptr",
"code_ref": "include/FreeRTOS/Task.hpp:110",
"snapshot_ref": "task.dispatch.vptr",
"evidence": "a0, object address, vptr, vtable entry and jalr target"
}
]
}
Required semantics:
id,numberandlabelare stable teaching identity;subtitlenames the actual mechanism and may differ between cards;sourceis the text source of the rendered asset;fallback_textis a compact readable map, not generated alt text;code_refis repository-relative and validated;- CODE steps may have no snapshot and never reset the program;
- RUN steps require a deterministic replay target;
evidencesays what proves the claim, not merely what should be visible;spread_groupjoins logical continuations without changing A4 print pages.
Plain technical rendering profile
"Plain text UML" means two related things in this project:
- diagrams are authored as reviewable text;
- their rendered form resembles a restrained technical drawing rather than a presentation slide.
The default profile therefore uses:
- white background, black/charcoal strokes and no shadows;
- square or nearly square corners;
- one monospaced family for identifiers, fields, methods and evidence;
- one sans-serif family only for explanatory prose;
- orthogonal relations where the renderer can keep labels legible;
- left-aligned members and short, explicit verbs on relations;
- no gradients, icons, ornamental colors or oversized titles;
- line weight and spacing, not color, as the primary print hierarchy;
- visible step anchors such as
01–08that remain useful in SVG and print; - two muted selectable tones in HTML and one subtle active outline;
- non-selectable content in neutral gray, never confused with a selectable inactive element.
PlantUML can generate ASCII/Unicode text output for sequence diagrams. We may ship that output as the fallback for sequences. Other diagram kinds receive a small hand-maintained text map because PlantUML's ASCII renderer does not cover all UML kinds. The fallback communicates topology and reading order; SVG and TeX remain the visual outputs.
Orientation, type scale and page visibility
The default reading direction is vertical. A horizontal layout is reserved for a genuine sequence or timeline whose primary fact is execution order over time. A binding chain is vertical unless time or participant lifelines are being modelled explicitly.
| Viewpoint | Default orientation | Reason |
|---|---|---|
| A1 CONTEXT | vertical | responsibility descends from application to platform |
| A2 STRUCTURE | vertical | base types, specializations and dependencies form tiers |
| A3 DISPATCH | vertical | binding stages form a top-to-bottom chain; use horizontal only for a genuine sequence |
| A4 APPLICATION | vertical | application objects lead to observers and invariants |
| A5 FLOW | horizontal | messages and time advance in reading order |
| A6 STATE | vertical | lifecycle transitions form explicit top-to-bottom paths |
| A7 RUNTIME | geometry-dependent | use vertical for an ownership descent, horizontal for a memory/CPU evidence pipeline |
| A8 PATTERNS | geometry-dependent | use vertical for tiers and horizontal for a short chain of roles and consequences |
Use top to bottom direction unless the diagram is a genuine sequence or
timeline (normally A5). A diagram must not be made horizontal merely to fill
page width.
Physical page orientation follows the rendered model rather than the viewpoint name: use one portrait page, one landscape page, or an explicit semantic page composition. Prefer a single page after a deliberate layout change; introduce tiles only when readable labels and complete semantic blocks still cannot fit.
The default PlantUML type scale is 14 for ordinary labels, 15 for class
names and 13 for class attributes. A smaller value is allowed only when the
diagram remains readable at the normal A4 viewer scale. Prefer fewer words,
shorter relation labels and a second logical page over shrinking text.
All diagrams belonging to one card use one shared PlantUML style include. The style is print-first and keeps the following invariants:
- one monospaced family for nodes, relations and sequence messages;
- a
190 pxminimum class width, while content may make a node wider; 1.5 pxnode borders and1.3 pxarrows;- a near-white node fill, blue-grey borders and dark text with sufficient monochrome-print contrast;
- the same base styling for class, state and sequence diagrams;
- no shadows, decorative gradients or per-diagram colour inventions.
Interactive selection is an overlay owned by the viewer. The source diagram must remain fully legible with no selection, while selectable nodes receive a clearer idle treatment than non-interactive context nodes. Selection changes emphasis, not geometry: it must not resize a node, move a label or alter the diagram layout.
At content scale = 100%, the HTML viewer preserves the intrinsic PlantUML
scale. A diagram that exceeds the declared A4-safe viewport is split into
semantic tiles; it is not silently compressed to width: 100%. The joined
HTML spread reconstructs the logical page in a CSS grid, while ordinary view
and print retain separate physical A4 sheets. Canvas zoom remains an explicit
viewer action and may be used to inspect the joined overview.
Every diagram starts on a new A4 page in HTML and TeX. Its SVG must fit within
the page-safe margins with all labels, arrowheads and numbered anchors visible.
Long diagrams continue on a new page with stable global step numbers and may be
joined by the HTML spread view. Every physical tile remains legible on its own,
but its part, row and column identify its exact position in the logical model.
Large diagrams and page continuity
A viewpoint is one logical model even when it needs several A4 pages.
page_griddeclares columns, rows and explicit semanticcuts_x/cuts_y;- a cut may cross whitespace or a connector, but never a class, object, state, participant, note, numbered anchor or other closed semantic block;
- the generator validates the rendered SVG geometry and rejects a colliding seam instead of shrinking the diagram or accepting a broken print;
- every interactive step belongs to exactly one tile through
step_tiles; - all tiles use one logical figure number and one global navigation cursor;
- each page carries
A<n>, the same viewpoint title,part x/y, and a sharedspread_group; - duplicated participants at a page boundary are marked
continued, not treated as new objects; - step numbers remain global across parts;
- connectors leaving a page use a named continuation token, for example
D3 -> A5.2; - HTML can join the parts side by side; print keeps independent A4 pages;
- the joined HTML view fits the complete logical row into the viewport, keeps page tops aligned and centers the semantic join rather than either sheet;
- a screen-only continuation cue in every part header indicates available neighbours to the left, right, above and below;
- selecting a step scrolls to the correct part and preserves one cursor;
- no information is encoded only in the physical proximity of two pages.
The source diagram may use a modest, explicit PlantUML scale when that is
necessary to place a semantic seam inside the A4-safe viewport. This is a
design decision, not automatic fit-to-width. The resulting normal print must
still meet the shared type-scale readability gate; HTML hover/canvas zoom is a
convenience and never a substitute for readable print.
Interaction and correspondence rules
The diagram, source tree and debugger share one navigation identity:
SERIES -> CARD -> TASK -> BLOCK -> VIEWPOINT/PHASE -> STEP -> SNAPSHOT
For every selectable anchor:
- its visible step number occurs exactly once inside that diagram;
- click and keyboard navigation select the same step;
- CODE opens the exact code or binary proof and does not mutate runtime;
- RUN opens code and may replay only after explicit activation;
- compile-time relations use code,
nm,readelfor disassembly evidence; - runtime messages use checkpoint evidence;
- focus is a subtle overlay; unselected selectable targets stay visible;
- a missing
snapshot_refis valid for CODE and invalid for RUN; - the authored SVG target is validated after PlantUML rendering.
Diagram-first teaching contract
The diagram is the primary entry point into an example. A printed card must teach the algorithm, responsibility boundary, state transition or dispatch mechanism before it exposes implementation syntax. Complete source listings do not belong on the card. The card carries the repository URL/QR code, while an interactive CODE anchor opens only the relevant symbol or source range in Neovim.
The intended learning path is:
goal and scope
-> relevant A1-A8 viewpoint
-> numbered diagram element
-> CODE source anchor or RUN checkpoint
-> Neovim / Termdebug / Hazard3 evidence
-> timestamped snapshot
Consequently:
- a card uses only the A1-A8 viewpoints which materially explain the subject;
- the diagram explains the mechanism, source confirms its implementation and the debugger supplies evidence;
- CODE navigation does not restart or mutate the program;
- RUN navigation may replay only to a declared deterministic checkpoint;
- source excerpts may appear temporarily in the interactive Neovim view, but are not duplicated as full static listings in HTML, TeX or print;
- every source jump is attached to a concrete diagram element, relation, message or state rather than to an unrelated code catalogue;
- the repository link remains the canonical route to the complete source.
This order is deliberate: students first reason about algorithms, states and responsibilities, then inspect the C or C++ syntax that implements them.
Editable UML layout and JSON persistence
Interactive UML views may expose an explicit EDIT mode. It applies only to
UML/model assets; it must not turn the whole card page into a free-form editor
or interfere with normal CODE/RUN navigation.
PlantUML remains the authoritative semantic source for participants, relationships and diagram kind. Manual visual decisions are stored in a sidecar JSON document named after the source, for example:
doc/assets/a2-structure.puml
doc/assets/a2-structure.layout.json
Saving a layout persists the complete authored presentation state, not only a temporary browser transform. At minimum the sidecar records:
- diagram identity, source revision and layout schema version;
- canvas size, page orientation, scale and optional page-grid metadata;
- stable element identity (
id/svg_target), position and stacking order; - width, height, local scale and rotation when the model kind allows it;
- displayed name, stereotype, short description and member text overrides;
- font family, font size, weight, style, line height and text alignment;
- fill, stroke, border width, corner treatment and semantic style role;
- group membership, collapsed state and explicit grouping/ungrouping;
- relation endpoints, labels and manually placed routing points;
- numbered CODE/RUN anchor placement without changing its teaching identity;
- editor metadata such as update timestamp and optional author.
A representative record is:
{
"schema": "stem.uml-layout/v1",
"diagram_id": "task01.a2-structure",
"source": "a2-structure.puml",
"source_digest": "sha256:...",
"canvas": {
"width": 1120,
"height": 760,
"orientation": "landscape",
"scale": 1
},
"elements": {
"task-base": {
"x": 310,
"y": 72,
"width": 360,
"height": 290,
"z": 2,
"text": {
"title": "Task<Derived>",
"description": "Static task wrapper",
"font_family": "IBM Plex Mono",
"font_size": 14,
"font_weight": 500,
"align": "left"
},
"style": {
"role": "wrapper",
"fill": "#eef5f8",
"stroke": "#397c99",
"stroke_width": 1.5
}
}
},
"relations": {
"task-to-api": {
"from": "task-base",
"to": "freertos-api",
"label": "calls",
"points": [[490, 362], [490, 430], [690, 430]]
}
},
"updated_at": "2026-07-19T12:00:00+02:00"
}
Editor requirements:
- dragging, resizing, text editing, grouping and relation routing update an in-memory draft first;
Save layoutvalidates stable IDs and writes the sidecar atomically;Cancelrestores the last saved sidecar without changing PlantUML;Reset layoutremoves visual overrides and returns to the generated PlantUML geometry only after explicit confirmation;- rerendering PlantUML reapplies compatible overrides by stable ID and reports orphaned overrides instead of silently discarding them;
- a source digest mismatch is visible to the editor but does not destroy the saved layout;
- normal view and print consume the same saved JSON, so the authored geometry is reproducible outside the editing session;
- CODE/RUN step IDs, source references and snapshot identities cannot be renamed accidentally by a visual edit.
Exact manual coordinates cannot be represented reliably through PlantUML
layout hints alone. The sidecar is therefore the canonical manual presentation
layer, while .puml remains the canonical semantic layer.
Two valid A3 profiles for FreeRTOS wrappers
The viewpoint set intentionally supports both of these designs.
Current teaching wrapper in K02
A2 STRUCTURE Task<Derived>, SumTask, SupervisorTask
A3 DISPATCH CRTP static dispatch
void* -> Task<Derived>* -> Derived* -> run()
A7 RUNTIME same object address; direct/inlined target; no vtable symbol
This matches the current K02 acceptance gate: no virtual dispatch, vtable, RTTI or hosted runtime.
jonenz/FreeRTOS-Cpp reference design
A2 STRUCTURE TaskBase <- Task / StaticTask<N> <- application task
A3 DISPATCH dynamic dispatch
void* -> TaskBase* -> taskEntry() -> virtual taskFunction()
A7 RUNTIME object address, vptr, vtable entry and indirect target
The upstream Task.hpp creates dynamic/static kernel tasks in constructors,
passes this to callTaskFunction(void*), and uses a virtual
taskFunction(). Its TaskBase destructor may call vTaskDelete(handle) when
configured. These are reference facts, not automatic design choices for our
teaching wrapper. Adopting that design would require changing K02's lifecycle,
ABI and no-vtable acceptance contracts explicitly.
Reference: https://github.com/jonenz/FreeRTOS-Cpp/blob/develop/FreeRTOS-Cpp/include/FreeRTOS/Task.hpp
Target profile: Boost.Asio and standard threads
This is the required future compatibility profile. It does not rename the fixed tabs. Mechanism-specific wording belongs in subtitles:
A1 CONTEXT · application, Asio, io_context, worker threads, OS
A2 STRUCTURE · Server, Session, socket, strand and ThreadPool
A3 DISPATCH · executor and completion-handler dispatch
A4 APPLICATION · one concrete Session or Client
A5 FLOW · asynchronous initiation and completion
A6 STATE · operation, session and thread lifecycles
A7 RUNTIME · threads, executors, handlers and owned buffers
A8 PATTERNS · Proactor and verified collaboration roles
The fixed label remains A5 FLOW, not A5 ASYNC FLOW; the UI subtitle may be
Async completion flow. Likewise the fixed label remains A7 RUNTIME, with
Threads / executors as a subtitle. This keeps cards comparable while still
naming the actual mechanism.
A1–A8 mapping
A1 CONTEXT shows this dependency/execution stack:
Application Session / Server
│ initiates
▼
Boost.Asio async operations, executors and strands
│ queue completions in
▼
io_context
│ run() is called by
▼
std::thread / std::jthread workers
│ use
▼
operating-system sockets and scheduling
A2 STRUCTURE shows the static ownership and dependency graph: Server,
Session, tcp::socket, executor/strand, buffers, io_context and the worker
collection. It must make lifetime ownership explicit, especially when a
session uses shared_from_this() to outlive an initiating call.
A3 DISPATCH traces an initiating function through the outstanding
operation, associated executor, completion queue and selected handler. It must
not draw async_read as if it created a user thread. Completion handlers are
invoked only by threads currently running io_context::run(); several such
threads form a pool from which io_context may choose. A strand adds strictly
sequential handler invocation, not physical affinity to one thread.
A4 APPLICATION collapses Asio infrastructure and shows one concrete
protocol/session contract, for example EchoSession::start/read/onRead/write.
A5 FLOW uses the explicit sequence:
initiate -> outstanding -> operation completes -> handler queued
-> one run() thread invokes handler -> next operation or completion
A6 STATE separates at least three domains when present:
jthread object: absent -> joinable -> stop requested -> joined
async operation: initiated -> outstanding -> queued -> running -> done
application Session: Created -> Reading -> Processing -> Writing -> Closed
The std::jthread destructor requests stop and joins when the object is still
joinable. A session state is application data; it must not be inferred merely
from a handler being present in a queue.
A7 RUNTIME records io_context, worker IDs, current handler and thread,
executor/strand identity, socket descriptor, operation kind, session address,
buffer lifetime and relevant smart-pointer ownership. The evidence view uses
info threads, backtraces and object inspection rather than RTOS TCB fields.
A8 PATTERNS may identify Proactor, callback/continuation, serialized
execution through a strand, thread pool, RAII thread ownership and lifetime
management. These are classified carefully: std::jthread ownership is a C++
idiom, a thread pool is a concurrency architecture, and not every completion
handler constitutes a GoF Command. Boost.Asio documents its asynchronous model
as Proactor; the card must still map each claimed role to project code.
The central invariant for every future Asio card is:
asynchronous operation != thread
operation completion -> associated executor -> io_context
-> one thread currently executing run()
Primary references for this profile:
- https://www.boost.org/doc/libs/latest/doc/html/boost_asio/overview/core/threads.html
- https://www.boost.org/doc/libs/latest/doc/html/boost_asio/overview/core/strands.html
- https://www.boost.org/doc/libs/latest/doc/html/boost_asio/overview/core/async.html
- https://eel.is/c++draft/thread.thread.class
- https://eel.is/c++draft/thread.jthread.class
Definition of done for a card view
A viewpoint is ready when:
- its purpose and question are stated;
- the chosen UML/model kind fits that question;
- every relation has one unambiguous meaning;
- every selectable item has a stable anchor and code reference;
- runtime claims have reproducible evidence;
- compile-time claims use compiler/binary evidence rather than fake runtime;
- the text fallback remains understandable without SVG;
- the view prints in monochrome and works as an interactive HTML asset;
- continuation across pages preserves identity and step order;
- A8 names costs and rejected alternatives, not only benefits.