node-red-contrib-s2 0.1.3 → 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.
Files changed (37) hide show
  1. package/README.md +36 -8
  2. package/dist/lib/index.d.ts +1 -1
  3. package/dist/lib/s2/schedule.d.ts +3 -2
  4. package/dist/lib/s2/schedule.js +8 -7
  5. package/dist/lib/s2/schedule.js.map +1 -1
  6. package/dist/lib/s2/session.d.ts +17 -22
  7. package/dist/lib/s2/session.js +29 -59
  8. package/dist/lib/s2/session.js.map +1 -1
  9. package/dist/nodes/s2-cem-config/index.html +10 -0
  10. package/dist/nodes/s2-ombc/icons/s2-logo.svg +14 -0
  11. package/dist/nodes/s2-ombc/index.d.ts +30 -0
  12. package/dist/nodes/s2-ombc/index.html +82 -0
  13. package/dist/nodes/s2-ombc/index.js +153 -0
  14. package/dist/nodes/s2-ombc/index.js.map +1 -0
  15. package/dist/nodes/s2-ombc-config/index.d.ts +3 -0
  16. package/dist/nodes/s2-ombc-config/index.html +428 -0
  17. package/dist/nodes/s2-ombc-config/index.js +9 -0
  18. package/dist/nodes/s2-ombc-config/index.js.map +1 -0
  19. package/dist/nodes/s2-pebc/icons/s2-logo.svg +14 -0
  20. package/dist/nodes/s2-pebc/index.d.ts +44 -0
  21. package/dist/nodes/s2-pebc/index.html +85 -0
  22. package/dist/nodes/s2-pebc/index.js +417 -0
  23. package/dist/nodes/s2-pebc/index.js.map +1 -0
  24. package/dist/nodes/s2-pebc-config/index.d.ts +3 -0
  25. package/dist/nodes/s2-pebc-config/index.html +64 -0
  26. package/dist/nodes/s2-pebc-config/index.js +10 -0
  27. package/dist/nodes/s2-pebc-config/index.js.map +1 -0
  28. package/dist/nodes/s2-rm/index.d.ts +21 -11
  29. package/dist/nodes/s2-rm/index.html +12 -88
  30. package/dist/nodes/s2-rm/index.js +85 -309
  31. package/dist/nodes/s2-rm/index.js.map +1 -1
  32. package/dist/nodes/s2-rm-config/index.html +34 -66
  33. package/dist/nodes/s2-rm-config/index.js +2 -2
  34. package/dist/nodes/s2-rm-config/index.js.map +1 -1
  35. package/dist/types/config-nodes.d.ts +20 -6
  36. package/package.json +6 -2
  37. package/resources/s2-styles.css +101 -2
@@ -1,14 +1,5 @@
1
1
  <script src="resources/node-red-contrib-s2/s2-common.js"></script>
2
2
  <script type="text/javascript">
3
- var GRID_CONNECTIONS = {
4
- '1x16A': 3680,
5
- '3x16A': 11040,
6
- '3x25A': 17250,
7
- '3x32A': 22080,
8
- '3x40A': 27600,
9
- '3x63A': 43470
10
- }
11
-
12
3
  RED.nodes.registerType('s2-rm-config', {
13
4
  category: 'config',
14
5
  defaults: {
@@ -21,10 +12,8 @@
21
12
  manufacturer: { value: 'Victron Energy' },
22
13
  model: { value: 'Virtual RM' },
23
14
  firmwareVersion: { value: '1.0.0' },
24
- maxBatteryChargePower: { value: 0, validate: RED.validators.number() },
25
- maxBatteryDischargePower: { value: 0, validate: RED.validators.number() },
26
- gridConnection: { value: '' },
27
- customMaxPowerW: { value: '' },
15
+ providesPowerMeasurement: { value: '3_PHASE_SYMMETRIC' },
16
+ providesForecast: { value: false },
28
17
  instructionPollIntervalMs: { value: 2000, validate: RED.validators.number() },
29
18
  skipInstructionStatus: { value: false }
30
19
  },
@@ -56,26 +45,20 @@
56
45
  $(this).prop('checked', savedRoles.indexOf($(this).val()) !== -1)
57
46
  })
58
47
 
59
- function updateBatteryLimitsVisibility () {
60
- var hasStorage = $('.s2-rm-config-role-checkbox[value="ENERGY_STORAGE"]').is(':checked')
61
- $('#s2-rm-config-battery-section').toggle(hasStorage)
62
- }
63
-
64
48
  $('.s2-rm-config-role-checkbox').on('change', function () {
65
49
  if ($('.s2-rm-config-role-checkbox:checked').length === 0) {
66
50
  $(this).prop('checked', true)
67
51
  }
68
- updateBatteryLimitsVisibility()
69
52
  })
70
53
 
71
- updateBatteryLimitsVisibility()
72
-
73
- function updateCustomPowerVisibility () {
74
- var val = $('#node-config-input-gridConnection').val()
75
- $('#s2-rm-config-custom-power-row').toggle(val === 'custom')
54
+ // Migrate legacy boolean to string value
55
+ if (this.providesPowerMeasurement === true) {
56
+ this.providesPowerMeasurement = '3_PHASE_SYMMETRIC'
57
+ $('#node-config-input-providesPowerMeasurement').val('3_PHASE_SYMMETRIC')
58
+ } else if (this.providesPowerMeasurement === false) {
59
+ this.providesPowerMeasurement = ''
60
+ $('#node-config-input-providesPowerMeasurement').val('')
76
61
  }
77
- $('#node-config-input-gridConnection').on('change', updateCustomPowerVisibility)
78
- updateCustomPowerVisibility()
79
62
 
80
63
  $('#node-config-input-skipInstructionStatus').prop('checked', this.skipInstructionStatus === true)
81
64
 
@@ -158,22 +141,20 @@
158
141
  </div>
159
142
 
160
143
  <div class="form-row">
161
- <label for="node-config-input-gridConnection"><i class="fa fa-plug"></i> Grid Fuse<i class="fa fa-info-circle tooltip-icon" data-tooltip="Fuse/breaker capacity at the grid connection point. Sets the outer PEBC power limits sent to the CEM as [-max, +max] watts."></i></label>
162
- <select id="node-config-input-gridConnection">
163
- <option value="">-- Not set --</option>
164
- <option value="1x16A">1x16A (3.7 kW)</option>
165
- <option value="3x16A">3x16A (11 kW)</option>
166
- <option value="3x25A">3x25A (17.3 kW)</option>
167
- <option value="3x32A">3x32A (22 kW)</option>
168
- <option value="3x40A">3x40A (27.6 kW)</option>
169
- <option value="3x63A">3x63A (43.5 kW)</option>
170
- <option value="custom">Custom</option>
144
+ <label for="node-config-input-providesPowerMeasurement"><i class="fa fa-bolt"></i> Power Meas.<i class="fa fa-info-circle tooltip-icon" data-tooltip="Commodity quantities for power measurement. Advertised to the CEM in ResourceManagerDetails."></i></label>
145
+ <select id="node-config-input-providesPowerMeasurement" style="width: 70%;">
146
+ <option value="">None</option>
147
+ <option value="3_PHASE_SYMMETRIC">3-phase symmetric</option>
148
+ <option value="L1_L2_L3">Per phase (L1, L2, L3)</option>
171
149
  </select>
172
150
  </div>
173
151
 
174
- <div class="form-row" id="s2-rm-config-custom-power-row" style="display:none">
175
- <label for="node-config-input-customMaxPowerW"><i class="fa fa-bolt"></i> Max Power (W)<i class="fa fa-info-circle tooltip-icon" data-tooltip="Maximum power in watts for a custom grid connection. Used as both import and export limit."></i></label>
176
- <input type="number" id="node-config-input-customMaxPowerW" placeholder="e.g. 15000" min="1">
152
+ <div class="form-row">
153
+ <label for="node-config-input-providesForecast">
154
+ <i class="fa fa-line-chart"></i> Forecast
155
+ </label>
156
+ <input type="checkbox" id="node-config-input-providesForecast" style="width: auto; margin-top: 6px;">
157
+ <span style="margin-left: 6px;">Provides power forecast</span>
177
158
  </div>
178
159
 
179
160
  <div class="form-row">
@@ -196,33 +177,6 @@
196
177
  <input type="text" id="node-config-input-firmwareVersion" placeholder="e.g. 1.0.0">
197
178
  </div>
198
179
 
199
- <div id="s2-rm-config-battery-section">
200
- <div class="form-row">
201
- <label class="s2-section-label">
202
- <i class="fa fa-battery-three-quarters"></i> Battery Limits
203
- <i class="fa fa-info-circle tooltip-icon" data-tooltip="Hardware ceiling for battery power. CEM instructions are clamped to these values. Set to 0 for no limit."></i>
204
- </label>
205
- </div>
206
-
207
- <div class="form-row s2-battery-row">
208
- <label for="node-config-input-maxBatteryChargePower">
209
- <i class="fa fa-arrow-circle-down"></i> Charge
210
- <i class="fa fa-info-circle tooltip-icon" data-tooltip="Maximum battery charge power in Watts. Never exceeded when writing DESS limits."></i>
211
- </label>
212
- <input type="number" id="node-config-input-maxBatteryChargePower" placeholder="0 = no limit" min="0" step="1">
213
- <span class="s2-unit-label">W</span>
214
- </div>
215
-
216
- <div class="form-row s2-battery-row">
217
- <label for="node-config-input-maxBatteryDischargePower">
218
- <i class="fa fa-arrow-circle-up"></i> Discharge
219
- <i class="fa fa-info-circle tooltip-icon" data-tooltip="Maximum battery discharge power in Watts. Never exceeded when writing DESS limits."></i>
220
- </label>
221
- <input type="number" id="node-config-input-maxBatteryDischargePower" placeholder="0 = no limit" min="0" step="1">
222
- <span class="s2-unit-label">W</span>
223
- </div>
224
- </div>
225
-
226
180
  <div class="form-row">
227
181
  <a href="#" class="s2-advanced-toggle" id="s2-rm-config-advanced-toggle">
228
182
  <i class="fa fa-cog"></i> Advanced
@@ -251,4 +205,18 @@
251
205
  </div>
252
206
  </script>
253
207
 
208
+ <script type="text/html" data-help-name="s2-rm-config">
209
+ <p>Configuration for <b>s2-rm</b>: the Resource Manager's identity, advertised capabilities, and protocol-level behavior. Sent to the CEM in <code>ResourceManagerDetails</code> during the S2 handshake.</p>
210
+
211
+ <h3>Identity</h3>
212
+ <p><b>Resource ID</b> is a stable UUID identifying this RM to the CEM - it's also used to build the CEM connection URL on <b>s2-cem-config</b>. Auto-generated the first time the node is added; avoid changing it after deployment, since the CEM associates state with this id.</p>
213
+ <p><b>Roles</b> and <b>Control Types</b> are advertised capabilities - at least one of each must be selected. Control types beyond OMBC and PEBC (FRBC, DDBC, PPBC) have no dedicated node yet; their instructions are still forwarded on <b>s2-rm</b>'s instructions output for you to handle yourself.</p>
214
+
215
+ <h3>Power Meas. / Forecast</h3>
216
+ <p>Advertise whether this RM reports <code>PowerMeasurement</code> (as a single 3-phase-symmetric value, or per-phase L1/L2/L3) and/or <code>PowerForecast</code>. These affect what's advertised to the CEM and whether <b>s2-rm</b> emits its <code>PowerMeasurementStart</code> signal on output 1 - they don't gate whether a <code>PowerMeasurement</code>/<code>Forecast</code> command you send is actually forwarded, which happens regardless.</p>
217
+
218
+ <h3>Advanced</h3>
219
+ <p><b>Poll interval</b> controls how often <b>s2-rm</b> checks for pending instructions whose <code>execution_time</code> has now arrived (PEBC instructions bypass this queue entirely and are delivered immediately). <b>Skip instruction status updates</b> suppresses automatic <code>InstructionStatusUpdate(ACCEPTED/STARTED)</code> messages - <code>ReceptionStatus</code> and control-type status messages are still sent - useful if the CEM generates high load from frequent instructions.</p>
220
+ </script>
221
+
254
222
  <link rel="stylesheet" href="resources/node-red-contrib-s2/s2-styles.css">
@@ -10,8 +10,8 @@ module.exports = function (RED) {
10
10
  this.model = config.model;
11
11
  this.firmwareVersion = config.firmwareVersion;
12
12
  this.controlTypes = config.controlTypes;
13
- this.gridConnection = config.gridConnection;
14
- this.customMaxPowerW = config.customMaxPowerW != null ? Number(config.customMaxPowerW) : undefined;
13
+ this.providesPowerMeasurement = config.providesPowerMeasurement;
14
+ this.providesForecast = config.providesForecast === true;
15
15
  this.instructionPollIntervalMs = config.instructionPollIntervalMs != null ? Number(config.instructionPollIntervalMs) : 2000;
16
16
  this.skipInstructionStatus = config.skipInstructionStatus === true;
17
17
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/nodes/s2-rm-config/index.ts"],"names":[],"mappings":";AAGA,iBAAS,UAAU,GAAe;IAChC,SAAS,yBAAyB,CAAwB,MAAkB;QAC1E,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAClC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAoB,CAAA;QAC7C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAgB,CAAA;QACrC,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAe,CAAA;QACnC,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAsB,CAAA;QACjD,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAsB,CAAA;QACjD,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAe,CAAA;QACnC,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,eAAyB,CAAA;QACvD,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAsB,CAAA;QACjD,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,cAAwB,CAAA;QACrD,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,eAAe,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAClG,IAAI,CAAC,yBAAyB,GAAG,MAAM,CAAC,yBAAyB,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QAC3H,IAAI,CAAC,qBAAqB,GAAG,MAAM,CAAC,qBAAqB,KAAK,IAAI,CAAA;IACpE,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,cAAc,EAAE,yBAAyB,CAAC,CAAA;AACnE,CAAC,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/nodes/s2-rm-config/index.ts"],"names":[],"mappings":";AAGA,iBAAS,UAAU,GAAe;IAChC,SAAS,yBAAyB,CAAwB,MAAkB;QAC1E,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAClC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAoB,CAAA;QAC7C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAgB,CAAA;QACrC,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAe,CAAA;QACnC,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAsB,CAAA;QACjD,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAsB,CAAA;QACjD,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAe,CAAA;QACnC,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,eAAyB,CAAA;QACvD,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAsB,CAAA;QACjD,IAAI,CAAC,wBAAwB,GAAG,MAAM,CAAC,wBAAkC,CAAA;QACzE,IAAI,CAAC,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,KAAK,IAAI,CAAA;QACxD,IAAI,CAAC,yBAAyB,GAAG,MAAM,CAAC,yBAAyB,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QAC3H,IAAI,CAAC,qBAAqB,GAAG,MAAM,CAAC,qBAAqB,KAAK,IAAI,CAAA;IACpE,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,cAAc,EAAE,yBAAyB,CAAC,CAAA;AACnE,CAAC,CAAA"}
@@ -12,12 +12,10 @@ export interface S2RmConfigNode extends NodeRedNode {
12
12
  model: string;
13
13
  firmwareVersion: string;
14
14
  controlTypes: string;
15
- /** Hardware ceiling for battery charge power in Watts. 0 = no limit configured. */
16
- maxBatteryChargePower: number;
17
- /** Hardware ceiling for battery discharge power in Watts. 0 = no limit configured. */
18
- maxBatteryDischargePower: number;
19
- gridConnection: string;
20
- customMaxPowerW: number | undefined;
15
+ /** Commodity quantity list for power measurement, or '' for none. */
16
+ providesPowerMeasurement: string;
17
+ /** Whether this RM provides power forecasts to the CEM. */
18
+ providesForecast: boolean;
21
19
  /** How often (ms) to poll for due pending instructions. Defaults to 2000 if not set. */
22
20
  instructionPollIntervalMs: number;
23
21
  /** When true, skip sending InstructionStatusUpdate(ACCEPTED/STARTED) automatically.
@@ -25,6 +23,22 @@ export interface S2RmConfigNode extends NodeRedNode {
25
23
  * acknowledgment messages and the extra D-Bus traffic causes CPU load. */
26
24
  skipInstructionStatus: boolean;
27
25
  }
26
+ /**
27
+ * Server-side shape of the s2-ombc-config config node.
28
+ * Use RED.nodes.getNode(id) and cast to this type.
29
+ */
30
+ export interface S2OmbcConfigNode extends NodeRedNode {
31
+ /** JSON-encoded OMBCSystemDescriptionConfig: { operationModes, transitions, timers } */
32
+ systemDescription: string;
33
+ }
34
+ /**
35
+ * Server-side shape of the s2-pebc-config config node.
36
+ * Use RED.nodes.getNode(id) and cast to this type.
37
+ */
38
+ export interface S2PebcConfigNode extends NodeRedNode {
39
+ gridConnection: string;
40
+ customMaxPowerW: number | undefined;
41
+ }
28
42
  /**
29
43
  * Server-side shape of the s2-cem-config config node.
30
44
  * Use RED.nodes.getNode(id) and cast to this type.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-s2",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Node-RED nodes for S2 energy management protocol (EN 50491-12-2)",
5
5
  "author": "Victron Energy BV",
6
6
  "repository": {
@@ -21,7 +21,11 @@
21
21
  "s2-rm-config": "./dist/nodes/s2-rm-config/index.js",
22
22
  "s2-cem-config": "./dist/nodes/s2-cem-config/index.js",
23
23
  "s2-websocket": "./dist/nodes/s2-websocket/index.js",
24
- "s2-rm": "./dist/nodes/s2-rm/index.js"
24
+ "s2-rm": "./dist/nodes/s2-rm/index.js",
25
+ "s2-ombc-config": "./dist/nodes/s2-ombc-config/index.js",
26
+ "s2-ombc": "./dist/nodes/s2-ombc/index.js",
27
+ "s2-pebc-config": "./dist/nodes/s2-pebc-config/index.js",
28
+ "s2-pebc": "./dist/nodes/s2-pebc/index.js"
25
29
  }
26
30
  },
27
31
  "scripts": {
@@ -69,8 +69,13 @@
69
69
  CONTROL TYPES / ROLES - Checkbox Grid
70
70
  ============================================================================ */
71
71
 
72
- /* Section labels (Roles, Control Types) span the full row. */
73
- .s2-form .s2-section-label {
72
+ /* Section labels (Roles, Control Types) span the full row.
73
+ Selector includes the `label` element type to match (and, combined with
74
+ source order, beat) .s2-form .form-row label's specificity above - a
75
+ class-only selector loses that tie-break despite both using !important,
76
+ leaving the label clipped to 120px with its text and tooltip icon
77
+ overlapping instead of spanning the row. */
78
+ .s2-form label.s2-section-label {
74
79
  width: 100% !important;
75
80
  display: inline-block;
76
81
  }
@@ -162,3 +167,97 @@
162
167
  color: #333;
163
168
  text-decoration: none;
164
169
  }
170
+
171
+ /* ============================================================================
172
+ OMBC SYSTEM DESCRIPTION - Friendly/Advanced tabs and operation-mode rows
173
+ ============================================================================ */
174
+
175
+ .s2-form .s2-ombc-tabs {
176
+ display: flex;
177
+ gap: 4px;
178
+ border-bottom: 1px solid #ccc;
179
+ margin-bottom: 8px;
180
+ }
181
+
182
+ .s2-form .s2-ombc-tab {
183
+ padding: 5px 12px;
184
+ color: #666;
185
+ text-decoration: none;
186
+ border: 1px solid transparent;
187
+ border-bottom: none;
188
+ border-radius: 3px 3px 0 0;
189
+ }
190
+
191
+ .s2-form .s2-ombc-tab:hover {
192
+ color: #333;
193
+ text-decoration: none;
194
+ }
195
+
196
+ .s2-form .s2-ombc-tab.s2-ombc-tab-active {
197
+ color: #333;
198
+ font-weight: 600;
199
+ border-color: #ccc;
200
+ background: #fff;
201
+ margin-bottom: -1px;
202
+ }
203
+
204
+ .s2-form .s2-ombc-mode-row {
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 10px;
208
+ flex-wrap: wrap;
209
+ }
210
+
211
+ .s2-form .s2-ombc-mode-row + .s2-ombc-mode-row {
212
+ margin-top: 4px;
213
+ }
214
+
215
+ .s2-form .s2-ombc-mode-row input[type="text"] {
216
+ flex: 1;
217
+ min-width: 100px;
218
+ width: auto !important;
219
+ }
220
+
221
+ .s2-form .s2-ombc-mode-row input[type="number"] {
222
+ width: 70px !important;
223
+ }
224
+
225
+ /* `label` element type included for the same specificity reason as
226
+ label.s2-section-label above. */
227
+ .s2-form label.s2-ombc-mode-checkbox-label {
228
+ display: inline-flex;
229
+ align-items: center;
230
+ gap: 4px;
231
+ width: auto !important;
232
+ white-space: nowrap;
233
+ font-weight: normal;
234
+ cursor: pointer;
235
+ }
236
+
237
+ .s2-form .s2-ombc-mode-checkbox-label input[type="checkbox"] {
238
+ width: auto !important;
239
+ }
240
+
241
+ .s2-form .s2-ombc-mode-value-wrap {
242
+ display: inline-flex;
243
+ align-items: center;
244
+ gap: 4px;
245
+ white-space: nowrap;
246
+ }
247
+
248
+ .s2-form .s2-ombc-mode-protected-note {
249
+ color: #888;
250
+ font-size: 0.85em;
251
+ font-style: italic;
252
+ margin-top: 2px;
253
+ }
254
+
255
+ .s2-form .s2-ombc-advanced-warning {
256
+ color: #a06400;
257
+ background: #fff8e6;
258
+ border: 1px solid #f0d78c;
259
+ border-radius: 3px;
260
+ padding: 6px 10px;
261
+ margin-bottom: 8px;
262
+ font-size: 0.9em;
263
+ }