Merge branch 'geoapi-3.1'.
This is mostly about changing the type of `TemporalDatum.origin` to `java.time`, at least internally.
diff --git a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/AboutCommand.java b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/AboutCommand.java
index ec5f061..3744340 100644
--- a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/AboutCommand.java
+++ b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/AboutCommand.java
@@ -36,7 +36,7 @@
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.collection.TableColumn;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.X364;
 import org.apache.sis.system.Loggers;
 import org.apache.sis.system.Supervisor;
@@ -139,7 +139,7 @@
                  * Logs a message telling how long it took to receive the reply.
                  * Sometimes the delay gives a hint about the server charge.
                  */
-                double delay = (System.nanoTime() - time) / (double) StandardDateFormat.NANOS_PER_SECOND;   // In seconds.
+                double delay = (System.nanoTime() - time) / (double) Constants.NANOS_PER_SECOND;   // In seconds.
                 if (delay >= 0.1) {
                     final double scale = (delay >= 10) ? 1 : (delay >= 1) ? 10 : 100;
                     delay = Math.rint(delay * scale) / scale;
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/FeatureMemoryBenchmark.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/FeatureMemoryBenchmark.java
index eb1cefe..f8256c1 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/FeatureMemoryBenchmark.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/FeatureMemoryBenchmark.java
@@ -21,7 +21,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.Constants;
 
 // Test dependencies
 import org.apache.sis.test.Benchmark;
@@ -182,7 +182,7 @@
                 long time = System.nanoTime();
                 b.run();
                 time = System.nanoTime() - time;
-                System.console().printf("Ellapsed time: %f%n", time / (float) StandardDateFormat.NANOS_PER_SECOND);
+                System.console().printf("Ellapsed time: %f%n", time / (float) Constants.NANOS_PER_SECOND);
                 return;
             }
         }
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/PeriodLiteral.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/PeriodLiteral.java
index 3c5b646..faa3eb8 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/PeriodLiteral.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/PeriodLiteral.java
@@ -16,13 +16,9 @@
  */
 package org.apache.sis.filter;
 
-import java.util.Date;
 import java.time.Instant;
 import java.io.Serializable;
 
-// Test dependencies
-import org.apache.sis.test.TestUtilities;
-
 // Specific to the main branch:
 import org.apache.sis.feature.AbstractFeature;
 import org.apache.sis.pending.geoapi.filter.Literal;
@@ -86,7 +82,7 @@
      */
     @Override
     public String toString() {
-        return "Period[" + TestUtilities.format(new Date(begin)) +
-                 " ... " + TestUtilities.format(new Date(end)) + ']';
+        return "Period[" + Instant.ofEpochMilli(begin) +
+                 " ... " + Instant.ofEpochMilli(end) + ']';
     }
 }
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/TemporalFilterTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/TemporalFilterTest.java
index f6fad0b..71c23ab 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/TemporalFilterTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/TemporalFilterTest.java
@@ -16,7 +16,7 @@
  */
 package org.apache.sis.filter;
 
-import static org.apache.sis.util.privy.StandardDateFormat.MILLISECONDS_PER_DAY;
+import static org.apache.sis.util.privy.Constants.MILLISECONDS_PER_DAY;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/acquisition/DefaultEvent.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/acquisition/DefaultEvent.java
index 49db214..e03d430 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/acquisition/DefaultEvent.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/acquisition/DefaultEvent.java
@@ -18,6 +18,7 @@
 
 import java.util.Collection;
 import java.util.Date;
+import java.time.temporal.Temporal;
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
@@ -30,8 +31,8 @@
 import org.opengis.metadata.acquisition.Sequence;
 import org.opengis.metadata.acquisition.Trigger;
 import org.apache.sis.metadata.iso.ISOMetadata;
-import static org.apache.sis.metadata.privy.ImplementationHelper.toDate;
-import static org.apache.sis.metadata.privy.ImplementationHelper.toMilliseconds;
+import static org.apache.sis.util.privy.TemporalDate.toDate;
+import static org.apache.sis.util.privy.TemporalDate.toInstant;
 
 
 /**
@@ -57,7 +58,7 @@
  *
  * @author  Cédric Briançon (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  * @since   0.3
  */
 @XmlType(name = "MI_Event_Type", propOrder = {
@@ -75,7 +76,7 @@
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = -519920133287763009L;
+    private static final long serialVersionUID = 7862440773058520852L;
 
     /**
      * Initiator of the event.
@@ -93,9 +94,10 @@
     private Sequence sequence;
 
     /**
-     * Time the event occurred, or {@link Long#MIN_VALUE} if none.
+     * Date and/or time the event occurred.
      */
-    private long time = Long.MIN_VALUE;
+    @SuppressWarnings("serial")     // Most implementations are serializable.
+    private Temporal time;
 
     /**
      * Objective or objectives satisfied by an event.
@@ -130,6 +132,7 @@
      *
      * @see #castOrCopy(Event)
      */
+    @SuppressWarnings("this-escape")
     public DefaultEvent(final Event object) {
         super(object);
         if (object != null) {
@@ -137,7 +140,7 @@
             trigger            = object.getTrigger();
             context            = object.getContext();
             sequence           = object.getSequence();
-            time               = toMilliseconds(object.getTime());
+            time               = toInstant(object.getTime());
             expectedObjectives = copyCollection(object.getExpectedObjectives(), Objective.class);
             relatedPass        = object.getRelatedPass();
             relatedSensors     = copyCollection(object.getRelatedSensors(), Instrument.class);
@@ -256,6 +259,10 @@
     /**
      * Returns the time the event occurred.
      *
+     * <div class="warning"><b>Upcoming API change — temporal schema</b><br>
+     * The return type of this method may change in a future version.
+     * It may be replaced by {@link Temporal}.</div>
+     *
      * @return time the event occurred, or {@code null}.
      */
     @Override
@@ -270,8 +277,19 @@
      * @param  newValue  the new time value.
      */
     public void setTime(final Date newValue) {
+        setTime(toInstant(newValue));
+    }
+
+    /**
+     * Sets the date and/or time the event occurred.
+     *
+     * @param  newValue  the new time value.
+     *
+     * @since 1.5
+     */
+    public void setTime(final Temporal newValue) {
         checkWritePermission(time);
-        time = toMilliseconds(newValue);
+        time = newValue;
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/acquisition/package-info.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/acquisition/package-info.java
index f2f1bf6..a9470eb 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/acquisition/package-info.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/acquisition/package-info.java
@@ -87,7 +87,7 @@
  * @author  Cédric Briançon (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Cullen Rombach (Image Matters)
- * @version 1.4
+ * @version 1.5
  * @since   0.3
  */
 @XmlSchema(location="https://schemas.isotc211.org/19115/-3/mac/1.0/mac.xsd",
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/DefaultCitationDate.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/DefaultCitationDate.java
index cb20155..ebf05da 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/DefaultCitationDate.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/DefaultCitationDate.java
@@ -17,6 +17,7 @@
 package org.apache.sis.metadata.iso.citation;
 
 import java.util.Date;
+import java.time.temporal.Temporal;
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
@@ -24,8 +25,8 @@
 import org.opengis.metadata.citation.DateType;
 import org.apache.sis.metadata.TitleProperty;
 import org.apache.sis.metadata.iso.ISOMetadata;
-import static org.apache.sis.metadata.privy.ImplementationHelper.toDate;
-import static org.apache.sis.metadata.privy.ImplementationHelper.toMilliseconds;
+import static org.apache.sis.util.privy.TemporalDate.toDate;
+import static org.apache.sis.util.privy.TemporalDate.toInstant;
 
 
 /**
@@ -47,7 +48,7 @@
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Cédric Briançon (Geomatys)
- * @version 1.4
+ * @version 1.5
  * @since   0.3
  */
 @TitleProperty(name = "date")
@@ -60,13 +61,13 @@
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = 5140213754542273710L;
+    private static final long serialVersionUID = 1032356967666782327L;
 
     /**
-     * Reference date for the cited resource in milliseconds elapsed sine January 1st, 1970,
-     * or {@link Long#MIN_VALUE} if none.
+     * Reference date for the cited resource.
      */
-    private long date = Long.MIN_VALUE;
+    @SuppressWarnings("serial")     // Most implementations are serializable.
+    private Temporal date;
 
     /**
      * Event used for reference date.
@@ -82,11 +83,28 @@
     /**
      * Constructs a citation date initialized to the given date.
      *
+     * @param date      the reference date for the cited resource, or {@code null} if unknown.
+     * @param dateType  the event used for reference date, or {@code null} if unknown.
+     *
+     * @since 1.5
+     */
+    public DefaultCitationDate(final Temporal date, final DateType dateType) {
+        this.date = date;
+        this.dateType = dateType;
+    }
+
+    /**
+     * Constructs a citation date initialized to the given date.
+     *
      * @param date      the reference date for the cited resource.
      * @param dateType  the event used for reference date.
+     *
+     * @deprecated Replaced by {@link #DefaultCitationDate(Temporal, DateType)}
+     * in order to transition to {@code java.time} API.
      */
+    @Deprecated(since="1.5", forRemoval=true)
     public DefaultCitationDate(final Date date, final DateType dateType) {
-        this.date = toMilliseconds(date);
+        this.date = toInstant(date);
         this.dateType = dateType;
     }
 
@@ -102,7 +120,7 @@
     public DefaultCitationDate(final CitationDate object) {
         super(object);
         if (object != null) {
-            date     = toMilliseconds(object.getDate());
+            date     = toInstant(object.getDate());
             dateType = object.getDateType();
         }
     }
@@ -135,6 +153,10 @@
     /**
      * Returns the reference date for the cited resource.
      *
+     * <div class="warning"><b>Upcoming API change — temporal schema</b><br>
+     * The return type of this method may change in a future version.
+     * It may be replaced by {@link Temporal}.</div>
+     *
      * @return reference date for the cited resource, or {@code null}.
      */
     @Override
@@ -149,8 +171,19 @@
      * @param  newValue  the new date.
      */
     public void setDate(final Date newValue) {
-        checkWritePermission(toDate(date));
-        date = toMilliseconds(newValue);
+        setDate(toInstant(newValue));
+    }
+
+    /**
+     * Sets the reference date for the cited resource.
+     *
+     * @param  newValue  the new date.
+     *
+     * @since 1.5
+     */
+    public void setDate(final Temporal newValue) {
+        checkWritePermission(date);
+        date = newValue;
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/package-info.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/package-info.java
index e314180..3d33901 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/package-info.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/package-info.java
@@ -87,7 +87,7 @@
  * @author  Touraïvane (IRD)
  * @author  Cédric Briançon (Geomatys)
  * @author  Cullen Rombach (Image Matters)
- * @version 1.4
+ * @version 1.5
  * @since   0.3
  */
 @XmlSchema(location="https://schemas.isotc211.org/19115/-3/cit/1.0/cit.xsd",
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultTemporalExtent.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultTemporalExtent.java
index 7e7ae5b..41e9069 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultTemporalExtent.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultTemporalExtent.java
@@ -17,7 +17,9 @@
 package org.apache.sis.metadata.iso.extent;
 
 import java.util.Date;
+import java.util.Optional;
 import java.time.Instant;
+import java.time.temporal.Temporal;
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlSeeAlso;
 import jakarta.xml.bind.annotation.XmlElement;
@@ -30,6 +32,7 @@
 import org.apache.sis.metadata.iso.ISOMetadata;
 import org.apache.sis.metadata.privy.ReferencingServices;
 import org.apache.sis.pending.temporal.TemporalUtilities;
+import org.apache.sis.util.privy.TemporalDate;
 import org.apache.sis.xml.NilObject;
 import org.apache.sis.xml.NilReason;
 
@@ -46,8 +49,8 @@
  *
  * In addition to the standard properties, SIS provides the following methods:
  * <ul>
- *   <li>{@link #getStartTime()} for fetching the start time from the temporal primitive.</li>
- *   <li>{@link #getEndTime()} for fetching the end time from the temporal primitive.</li>
+ *   <li>{@link #getBeginning()} for fetching the start time from the temporal primitive.</li>
+ *   <li>{@link #getEnding()} for fetching the end time from the temporal primitive.</li>
  *   <li>{@link #setBounds(Date, Date)} for setting the extent from the given start and end time.</li>
  *   <li>{@link #setBounds(Envelope)} for setting the extent from the given envelope.</li>
  * </ul>
@@ -138,9 +141,6 @@
 
     /**
      * Returns the date and time for the content of the dataset.
-     * If no extent has been {@linkplain #setExtent(TemporalPrimitive) explicitly set},
-     * then this method will build an extent from the {@linkplain #getStartTime() start
-     * time} and {@linkplain #getEndTime() end time} if any.
      *
      * @return the date and time for the content, or {@code null}.
      */
@@ -161,31 +161,56 @@
     }
 
     /**
-     * Infers a value from the extent as a {@link Date} object.
+     * Infers a value from the extent as a {@code Instant} object.
      *
-     * @param  begin  {@code true} if we are asking for the start time,
-     *                or {@code false} for the end time.
-     * @return the requested time as a Java date, or {@code null} if none.
+     * @param  begin  {@code true} for the start time, or {@code false} for the end time.
+     * @return the requested time as an instant, or {@code null} if none.
      */
-    static Date getTime(final TemporalPrimitive extent, final boolean begin) {
+    static Instant getBound(final TemporalPrimitive extent, final boolean begin) {
         if (extent instanceof Period) {
             var p = (Period) extent;
-            Instant time = begin ? p.getBeginning() : p.getEnding();
-            if (time != null) {
-                return Date.from(time);
-            }
+            return begin ? p.getBeginning() : p.getEnding();
         }
         return null;
     }
 
     /**
+     * Returns the start of the temporal range for the content of the dataset.
+     * This method tries to infer this value from the {@linkplain #getExtent() extent}.
+     * The returned object is often an {@link Instant}, but not necessarily.
+     *
+     * @return the start of the temporal range.
+     *
+     * @since 1.5
+     */
+    public Optional<Temporal> getBeginning() {
+        return Optional.ofNullable(getBound(extent, true));
+    }
+
+    /**
+     * Returns the end of the temporal range for the content of the dataset.
+     * This method tries to infer this value from the {@linkplain #getExtent() extent}.
+     * The returned object is often an {@link Instant}, but not necessarily.
+     *
+     * @return the end of the temporal range.
+     *
+     * @since 1.5
+     */
+    public Optional<Temporal> getEnding() {
+        return Optional.ofNullable(getBound(extent, false));
+    }
+
+    /**
      * The start date and time for the content of the dataset.
      * This method tries to infer it from the {@linkplain #getExtent() extent}.
      *
      * @return the start time, or {@code null} if none.
+     *
+     * @deprecated Replaced by {@link #getBeginning()} in order to transition to {@code java.time} API.
      */
+    @Deprecated(since="1.5", forRemoval=true)
     public Date getStartTime() {
-        return getTime(extent, true);
+        return TemporalDate.toDate(getBeginning().orElse(null));
     }
 
     /**
@@ -193,9 +218,12 @@
      * This method tries to infer it from the {@linkplain #getExtent() extent}.
      *
      * @return the end time, or {@code null} if none.
+     *
+     * @deprecated Replaced by {@link #getEnding()} in order to transition to {@code java.time} API.
      */
+    @Deprecated(since="1.5", forRemoval=true)
     public Date getEndTime() {
-        return getTime(extent, false);
+        return TemporalDate.toDate(getEnding().orElse(null));
     }
 
     /**
@@ -204,13 +232,26 @@
      *
      * @param  startTime  the start date and time for the content of the dataset, or {@code null} if none.
      * @param  endTime    the end date and time for the content of the dataset, or {@code null} if none.
+     *
+     * @deprecated Replaced by {@link #setBounds(Temporal, Temporal)} in order to transition to {@code java.time} API.
      */
-    public void setBounds(final Date startTime, final Date endTime) throws UnsupportedOperationException {
-        TemporalPrimitive value = null;
-        if (startTime != null || endTime != null) {
-            value = TemporalUtilities.createPeriod(startTime, endTime);
-        }
-        setExtent(value);
+    @Deprecated(since="1.5", forRemoval=true)
+    public void setBounds(final Date startTime, final Date endTime) {
+        setBounds((startTime == null) ? null : startTime.toInstant(),
+                    (endTime == null) ? null : endTime.toInstant());
+    }
+
+    /**
+     * Sets the temporal extent to the specified values. This convenience method creates a temporal
+     * primitive for the given dates and/or times, then invokes {@link #setExtent(TemporalPrimitive)}.
+     *
+     * @param  startTime  the start date and time for the content of the dataset, or {@code null} if none.
+     * @param  endTime    the end date and time for the content of the dataset, or {@code null} if none.
+     *
+     * @since 1.5
+     */
+    public void setBounds(final Temporal startTime, final Temporal endTime) {
+        setExtent(TemporalUtilities.createPeriod(startTime, endTime));
     }
 
     /**
@@ -222,7 +263,7 @@
      * module is available on the module path.</p>
      *
      * @param  envelope  the envelope to use for setting this temporal extent.
-     * @throws UnsupportedOperationException if the referencing module or the temporal module is not on the module path.
+     * @throws UnsupportedOperationException if the referencing module is not on the module path.
      * @throws TransformException if the envelope cannot be transformed to a temporal extent.
      *
      * @see DefaultExtent#addElements(Envelope)
@@ -255,21 +296,21 @@
             if (extent == null || (ot instanceof NilObject)) {
                 extent = ot;
             } else {
-                Date t0 = getTime(extent, true);
-                Date t1 = getTime(extent, false);
-                Date h0 = getTime(ot,     true);
-                Date h1 = getTime(ot,     false);
+                Instant t0 = getBound(extent, true);
+                Instant t1 = getBound(extent, false);
+                Instant h0 = getBound(ot,     true);
+                Instant h1 = getBound(ot,     false);
                 boolean changed = false;
-                if (h0 != null && (t0 == null || h0.after(t0))) {
+                if (h0 != null && (t0 == null || h0.isAfter(t0))) {
                     t0 = h0;
                     changed = true;
                 }
-                if (h1 != null && (t1 == null || h1.before(t1))) {
+                if (h1 != null && (t1 == null || h1.isBefore(t1))) {
                     t1 = h1;
                     changed = true;
                 }
                 if (changed) {
-                    if (t0 != null && t1 != null && t0.after(t1)) {
+                    if (t0 != null && t1 != null && t0.isAfter(t1)) {
                         extent = NilReason.MISSING.createNilObject(TemporalPrimitive.class);
                     } else {
                         setBounds(t0, t1);
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/Extents.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/Extents.java
index d5690b8..2d45a32 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/Extents.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/Extents.java
@@ -28,7 +28,11 @@
 import java.util.stream.Stream;
 import java.util.function.Function;
 import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
 import static java.lang.Math.*;
+import java.time.ZoneId;
+import java.time.Instant;
+import java.time.DateTimeException;
 import javax.measure.Unit;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
@@ -57,6 +61,7 @@
 import org.apache.sis.measure.Longitude;
 import org.apache.sis.measure.MeasurementRange;
 import org.apache.sis.measure.Range;
+import org.apache.sis.pending.jdk.JDK23;
 import org.apache.sis.util.OptionalCandidate;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ComparisonMode;
@@ -65,8 +70,9 @@
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.resources.Errors;
-import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
+import org.apache.sis.util.privy.TemporalDate;
 import static org.apache.sis.util.privy.CollectionsExt.nonNull;
+import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
 import static org.apache.sis.metadata.privy.ReferencingServices.AUTHALIC_RADIUS;
 
 // Specific to the main branch:
@@ -485,31 +491,53 @@
      * @return a time range created from the given extent, or {@code null} if none.
      *
      * @since 0.4
+     *
+     * @deprecated Replaced by {@link #getTimeRange(Extent, ZoneId)} in order to transition to {@code java.time} API.
      */
-    @OptionalCandidate
+    @Deprecated(since="1.5", forRemoval=true)
     public static Range<Date> getTimeRange(final Extent extent) {
-        Date min = null;
-        Date max = null;
-        if (extent != null) {
-            for (final TemporalExtent t : nonNull(extent.getTemporalElements())) {
-                final Date startTime, endTime;
-                if (t instanceof DefaultTemporalExtent) {
-                    final DefaultTemporalExtent dt = (DefaultTemporalExtent) t;
-                    startTime = dt.getStartTime();                  // Maybe user has overridden those methods.
-                    endTime   = dt.getEndTime();
-                } else {
-                    final TemporalPrimitive p = t.getExtent();
-                    startTime = DefaultTemporalExtent.getTime(p, true);
-                    endTime   = DefaultTemporalExtent.getTime(p, false);
-                }
-                if (startTime != null && (min == null || startTime.before(min))) min = startTime;
-                if (  endTime != null && (max == null ||   endTime.after (max))) max =   endTime;
+        return onTimeRange(extent, null, (min, max) -> {
+            if (min == null && max == null) {
+                return null;
             }
-        }
-        if (min == null && max == null) {
-            return null;
-        }
-        return new Range<>(Date.class, min, true, max, true);
+            return new Range<>(Date.class, TemporalDate.toDate(min), true, TemporalDate.toDate(max), true);
+        }).orElse(null);
+    }
+
+    /**
+     * Returns the union of all time ranges found in the given extent.
+     *
+     * @param  extent  the extent to convert to a time range, or {@code null}.
+     * @param  zone    the timezone to use if a time is local, or {@code null} if none.
+     * @return a time range created from the given extent.
+     * @throws DateTimeException if a temporal value cannot be converted to an instant.
+     *
+     * @since 1.5
+     */
+    public static Optional<Range<Instant>> getTimeRange(final Extent extent, final ZoneId zone) {
+        return onTimeRange(extent, zone, (min, max) -> {
+            if (min == null && max == null) {
+                return null;
+            }
+            return new Range<>(Instant.class, min, true, max, true);
+        });
+    }
+
+    /**
+     * Returns a date in the {@linkplain Extent#getTemporalElements() temporal elements} of the given extent.
+     *
+     * @param  extent    the extent from which to get an instant, or {@code null}.
+     * @param  location  0 for the start time, 1 for the end time, 0.5 for the average time, or the
+     *                   coefficient (usually in the [0 … 1] range) for interpolating an instant.
+     * @return an instant interpolated at the given location, or {@code null} if none.
+     *
+     * @since 0.4
+     *
+     * @deprecated Replaced by {@link #getTimeRange(Extent, ZoneId)} in order to transition to {@code java.time} API.
+     */
+    @Deprecated(since="1.5", forRemoval=true)
+    public static Date getDate(final Extent extent, final double location) {
+        return TemporalDate.toDate(getInstant(extent, null, location).orElse(null));
     }
 
     /**
@@ -523,8 +551,8 @@
      *
      * Special cases:
      * <ul>
-     *   <li>If {@code location} is 0, then this method returns the {@linkplain DefaultTemporalExtent#getStartTime() start time}.</li>
-     *   <li>If {@code location} is 1, then this method returns the {@linkplain DefaultTemporalExtent#getEndTime() end time}.</li>
+     *   <li>If {@code location} is 0, then this method returns the {@linkplain DefaultTemporalExtent#getBeginning() start time}.</li>
+     *   <li>If {@code location} is 1, then this method returns the {@linkplain DefaultTemporalExtent#getEnding() end time}.</li>
      *   <li>If {@code location} is 0.5, then this method returns the average of start time and end time.</li>
      *   <li>If {@code location} is outside the [0 … 1] range, then the result will be outside the temporal extent.</li>
      * </ul>
@@ -534,34 +562,49 @@
      *                   coefficient (usually in the [0 … 1] range) for interpolating an instant.
      * @return an instant interpolated at the given location, or {@code null} if none.
      *
-     * @since 0.4
+     * @since 1.5
      */
-    @OptionalCandidate
-    public static Date getDate(final Extent extent, final double location) {
+    public static Optional<Instant> getInstant(final Extent extent, final ZoneId zone, final double location) {
         ArgumentChecks.ensureFinite("location", location);
-        Date min = null;
-        Date max = null;
+        return onTimeRange(extent, zone, (min, max) -> {
+            if (min == null) return max;
+            if (max == null) return min;
+            return min.plusMillis(Math.round(location * JDK23.until(min, max).toMillis()));
+        });
+    }
+
+    /**
+     * Returns the result of applying the given function on the minimum and maximal temporal value.
+     *
+     * @param  <T>       type of value computed by the function.
+     * @param  extent    the extent on which to apply a function, or {@code null}.
+     * @param  zone      the timezone to use if a time is local, or {@code null} if none.
+     * @param  operator  the function to apply on the start and end time. Those times may be null.
+     * @return the result of applying the given function on the start and end time.
+     */
+    private static <T> Optional<T> onTimeRange(final Extent extent, final ZoneId zone,
+            final BiFunction<Instant,Instant,T> operator)
+    {
+        Instant min = null;
+        Instant max = null;
         if (extent != null) {
             for (final TemporalExtent t : nonNull(extent.getTemporalElements())) {
-                Date startTime = null;
-                Date   endTime = null;
+                final Instant startTime, endTime;
                 if (t instanceof DefaultTemporalExtent) {
-                    final DefaultTemporalExtent dt = (DefaultTemporalExtent) t;
-                    if (location != 1) startTime = dt.getStartTime();       // Maybe user has overridden those methods.
-                    if (location != 0)   endTime = dt.getEndTime();
+                    final var dt = (DefaultTemporalExtent) t;
+                    // Maybe user has overridden those methods.
+                    startTime = TemporalDate.toInstant(dt.getBeginning().orElse(null), zone);
+                    endTime   = TemporalDate.toInstant(dt.getEnding()   .orElse(null), zone);
                 } else {
                     final TemporalPrimitive p = t.getExtent();
-                    if (location != 1) startTime = DefaultTemporalExtent.getTime(p, true);
-                    if (location != 0)   endTime = DefaultTemporalExtent.getTime(p, false);
+                    startTime = DefaultTemporalExtent.getBound(p, true);
+                    endTime   = DefaultTemporalExtent.getBound(p, false);
                 }
-                if (startTime != null && (min == null || startTime.before(min))) min = startTime;
-                if (  endTime != null && (max == null ||   endTime.after (max))) max =   endTime;
+                if (startTime != null && (min == null || startTime.isBefore(min))) min = startTime;
+                if (  endTime != null && (max == null ||   endTime.isAfter (max))) max =   endTime;
             }
         }
-        if (min == null) return max;
-        if (max == null) return min;
-        final long startTime = min.getTime();
-        return new Date(Math.addExact(startTime, Math.round((max.getTime() - startTime) * location)));
+        return Optional.ofNullable(operator.apply(min, max));
     }
 
     /**
@@ -789,7 +832,7 @@
      * @param  e2           the second extent, or {@code null}.
      * @param  constructor  copy constructor of metadata implementation class.
      * @param  operator     the union or intersection operator to apply.
-     * @return
+     * @return the intersection or union of the given metadata objects.
      */
     @SuppressWarnings("unchecked")      // Workaround for Java above-cited compiler restriction.
     private static <I, C extends ISOMetadata> I apply(final I e1, final I e2,
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/legacy/TemporalToDate.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/legacy/TemporalToDate.java
index 6b71eff..2249cde 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/legacy/TemporalToDate.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/legacy/TemporalToDate.java
@@ -16,13 +16,12 @@
  */
 package org.apache.sis.metadata.iso.legacy;
 
-import java.time.Instant;
-import java.time.temporal.ChronoField;
 import java.time.temporal.Temporal;
 import java.util.AbstractCollection;
 import java.util.Collection;
 import java.util.Date;
 import java.util.Iterator;
+import org.apache.sis.util.privy.TemporalDate;
 
 
 /**
@@ -59,6 +58,7 @@
 
     /**
      * Returns an iterator over the dates in this collection.
+     * If a temporal object does not specify its timezone, then UTC is assumed.
      *
      * @return an iterator over the dates.
      */
@@ -73,13 +73,8 @@
 
             /** Returns the next temporal object, converted to a date. */
             @Override public Date next() {
-                final Temporal t = dates.next();
-                if (t == null) return null;
-                if (t instanceof Instant) {
-                    return Date.from((Instant) t);
-                }
-                // Following may throw `DateTimeException` if the temporal does not support the field.
-                return new Date(Math.multiplyExact(t.getLong(ChronoField.INSTANT_SECONDS), 1000));
+                // Following may throw `DateTimeException` if the temporal does not support a required field.
+                return TemporalDate.toDate(dates.next());
             }
 
             /** Remove the last date returned by the iterator. */
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/lineage/DefaultProcessStep.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/lineage/DefaultProcessStep.java
index 45ea41c..9fa6dcf 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/lineage/DefaultProcessStep.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/lineage/DefaultProcessStep.java
@@ -34,9 +34,9 @@
 import org.apache.sis.metadata.iso.ISOMetadata;
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.xml.bind.FilterByVersion;
-import org.apache.sis.xml.privy.LegacyNamespaces;
 import org.apache.sis.xml.bind.gml.TM_Primitive;
 import org.apache.sis.xml.bind.metadata.MD_Scope;
+import org.apache.sis.xml.privy.LegacyNamespaces;
 import org.apache.sis.pending.temporal.TemporalUtilities;
 
 // Specific to the main and geoapi-3.1 branches:
@@ -317,7 +317,7 @@
      */
     @Deprecated(since="1.0")
     public void setDate(final Date newValue) {
-        setStepDateTime(TemporalUtilities.createInstant(newValue));
+        setStepDateTime(TemporalUtilities.createInstant(newValue == null ? null : newValue.toInstant()));
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/maintenance/DefaultMaintenanceInformation.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/maintenance/DefaultMaintenanceInformation.java
index 2cec5bf..2d813be 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/maintenance/DefaultMaintenanceInformation.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/maintenance/DefaultMaintenanceInformation.java
@@ -160,6 +160,7 @@
      *
      * @see #castOrCopy(MaintenanceInformation)
      */
+    @SuppressWarnings("this-escape")
     public DefaultMaintenanceInformation(final MaintenanceInformation object) {
         super(object);
         if (object != null) {
@@ -305,7 +306,8 @@
             }
         }
         if (newValue != null) {
-            final CitationDate date = new DefaultCitationDate(newValue, NEXT_UPDATE);
+            @SuppressWarnings("removal")
+            final var date = new DefaultCitationDate(newValue, NEXT_UPDATE);
             if (dates != null) {
                 dates.add(date);
             } else {
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/ImplementationHelper.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/ImplementationHelper.java
index 84ab9d4..def420f 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/ImplementationHelper.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/ImplementationHelper.java
@@ -54,7 +54,7 @@
 
     /**
      * Returns the milliseconds value of the given date, or {@link Long#MIN_VALUE}
-     * if the date us null.
+     * if the date is null.
      *
      * @param  value  the date, or {@code null}.
      * @return the time in milliseconds, or {@code Long.MIN_VALUE} if none.
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/pending/temporal/TemporalUtilities.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/pending/temporal/TemporalUtilities.java
index c83c97f..aaa0a1b 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/pending/temporal/TemporalUtilities.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/pending/temporal/TemporalUtilities.java
@@ -18,7 +18,10 @@
 
 import java.util.Date;
 import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.temporal.Temporal;
 import org.opengis.temporal.TemporalPrimitive;
+import org.apache.sis.util.privy.TemporalDate;
 
 // Specific to the main branch:
 import org.apache.sis.pending.geoapi.temporal.Period;
@@ -42,27 +45,44 @@
      *
      * @param  time  the date for which to create instant, or {@code null}.
      * @return the instant, or {@code null} if the given time was null.
-     * @throws UnsupportedOperationException if the temporal factory is not available on the module path.
      */
-    public static TemporalPrimitive createInstant(final Date time) throws UnsupportedOperationException {
-        if (time == null) return null;
-        final Instant t = time.toInstant();
-        return new DefaultPeriod(t, t);
+    public static TemporalPrimitive createInstant(final Date time) {
+        return (time == null) ? null : createInstant(time.toInstant());
     }
 
     /**
-     * Creates a period for the given begin and end dates.
+     * Creates an instant for the given Java temporal instant.
      *
-     * @param  begin  the begin date (inclusive), or {@code null}.
-     * @param  end    the end date (inclusive), or {@code null}.
-     * @return the period, or {@code null} if both arguments are null.
-     * @throws UnsupportedOperationException if the temporal factory is not available on the module path.
+     * @param  time  the date for which to create instant, or {@code null}.
+     * @return the instant, or {@code null} if the given time was null.
      */
-    public static TemporalPrimitive createPeriod(final Date begin, final Date end) throws UnsupportedOperationException {
-        if (begin == null && end == null) return null;
-        return new DefaultPeriod(
-                (begin != null) ? begin.toInstant() : null,
-                  (end != null) ?   end.toInstant() : null);
+    public static TemporalPrimitive createInstant(final Instant time) {
+        return (time == null) ? null : new DefaultPeriod(time, time);
+    }
+
+    /**
+     * Creates a period for the given begin and end instant.
+     *
+     * @param  begin  the begin instant (inclusive), or {@code null}.
+     * @param  end    the end instant (inclusive), or {@code null}.
+     * @return the period, or {@code null} if both arguments are null.
+     */
+    public static TemporalPrimitive createPeriod(final Instant begin, final Instant end) {
+        return (begin == null && end == null) ? null : new DefaultPeriod(begin, end);
+    }
+
+    /**
+     * Creates a period for the given begin and end instant.
+     *
+     * @param  begin  the begin instant (inclusive), or {@code null}.
+     * @param  end    the end instant (inclusive), or {@code null}.
+     * @return the period, or {@code null} if both arguments are null.
+     *
+     * @todo Needs to avoid assuming UTC timezone.
+     */
+    public static TemporalPrimitive createPeriod(final Temporal begin, final Temporal end) {
+        return createPeriod(TemporalDate.toInstant(begin, ZoneOffset.UTC),
+                            TemporalDate.toInstant(end,   ZoneOffset.UTC));
     }
 
     /**
@@ -122,7 +142,7 @@
             var p = (Period) time;
             Instant instant;
             if ((instant = p.getEnding()) != null || (instant = p.getBeginning()) != null) {
-                return Date.from(instant);
+                return TemporalDate.toDate(instant);
             }
         }
         return null;
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TM_Primitive.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TM_Primitive.java
index 05c5097..5fcac07 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TM_Primitive.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TM_Primitive.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.xml.bind.gml;
 
-import java.util.Date;
 import java.time.Instant;
 import jakarta.xml.bind.annotation.XmlElement;
 import org.opengis.temporal.TemporalPrimitive;
@@ -108,10 +107,10 @@
         metadata = null;                                        // Cleaned first in case of failure.
         if (period != null) {
             final Context context = Context.current();
-            final Date begin = toDate(context, period.begin);
-            final Date end   = toDate(context, period.end);
+            final Instant begin = toInstant(context, period.begin);
+            final Instant end   = toInstant(context, period.end);
             if (begin != null || end != null) {
-                if (begin != null && end != null && end.before(begin)) {
+                if (begin != null && end != null && end.isBefore(begin)) {
                     /*
                      * Be tolerant - we can treat such case as an empty range, which is a similar
                      * approach to what JDK does for Rectangle width and height. We will log with
@@ -120,11 +119,9 @@
                      */
                     Context.warningOccured(context, TemporalPrimitive.class,
                             "setTimePeriod", Errors.class, Errors.Keys.IllegalRange_2, begin, end);
-                } else try {
+                } else {
                     metadata = TemporalUtilities.createPeriod(begin, end);
                     period.copyIdTo(metadata);
-                } catch (UnsupportedOperationException e) {
-                    warningOccured("setTimePeriod", e);
                 }
             }
         }
@@ -139,31 +136,19 @@
     public final void setTimeInstant(final TimeInstant instant) {
         metadata = null;                                        // Cleaned first in case of failure.
         if (instant != null) {
-            final Date position = XmlUtilities.toDate(Context.current(), instant.timePosition);
-            if (position != null) try {
+            final Instant position = XmlUtilities.toInstant(Context.current(), instant.timePosition);
+            if (position != null) {
                 metadata = TemporalUtilities.createInstant(position);
                 instant.copyIdTo(metadata);
-            } catch (UnsupportedOperationException e) {
-                warningOccured("setTimeInstant", e);
             }
         }
     }
 
     /**
-     * Returns the date of the given bounds, or {@code null} if none.
+     * Returns the instant of the given bounds, or {@code null} if none.
      */
-    private static Date toDate(final Context context, final TimePeriodBound bound) {
-        return (bound != null) ? XmlUtilities.toDate(context, bound.calendar()) : null;
-    }
-
-    /**
-     * Reports a warning for the given exception.
-     *
-     * @param method  the name of the method to declare in the log record.
-     * @param e the exception.
-     */
-    private static void warningOccured(final String method, final Exception e) {
-        Context.warningOccured(Context.current(), TM_Primitive.class, method, e, true);
+    private static Instant toInstant(final Context context, final TimePeriodBound bound) {
+        return (bound != null) ? XmlUtilities.toInstant(context, bound.calendar()) : null;
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TemporalAdapter.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TemporalAdapter.java
new file mode 100644
index 0000000..7d9416d
--- /dev/null
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TemporalAdapter.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.xml.bind.gml;
+
+import java.time.temporal.Temporal;
+import javax.xml.datatype.XMLGregorianCalendar;
+import javax.xml.datatype.DatatypeConfigurationException;
+import jakarta.xml.bind.annotation.adapters.XmlAdapter;
+import org.apache.sis.xml.privy.XmlUtilities;
+import org.apache.sis.xml.bind.Context;
+
+
+/**
+ * JAXB adapter wrapping a temporal value.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class TemporalAdapter extends XmlAdapter<XMLGregorianCalendar, Temporal> {
+    /**
+     * Empty constructor for JAXB only.
+     */
+    public TemporalAdapter() {
+    }
+
+    /**
+     * Converts a date read from a XML stream to the object which will contain the value.
+     * JAXB calls automatically this method at unmarshalling time.
+     *
+     * @param  value  the XML date, or {@code null}.
+     * @return the temporal object, or {@code null}.
+     */
+    @Override
+    public Temporal unmarshal(final XMLGregorianCalendar value) {
+        return XmlUtilities.toTemporal(Context.current(), value);
+    }
+
+    /**
+     * Converts the date to the object to be marshalled in a XML file or stream.
+     * JAXB calls automatically this method at marshalling time.
+     *
+     * @param  value  the temporal object, or {@code null}.
+     * @return the XML date, or {@code null}.
+     */
+    @Override
+    public XMLGregorianCalendar marshal(final Temporal value) {
+        try {
+            return XmlUtilities.toXML(Context.current(), value);
+        } catch (DatatypeConfigurationException e) {
+            Context.warningOccured(Context.current(), TemporalAdapter.class, "marshal", e, true);
+            return null;
+        }
+    }
+}
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TimePeriodBound.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TimePeriodBound.java
index 288abe6..da5c230 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TimePeriodBound.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TimePeriodBound.java
@@ -163,6 +163,7 @@
          */
         @Override
         XMLGregorianCalendar calendar() {
+            @SuppressWarnings("LocalVariableHidesMemberVariable")
             final TimeInstant timeInstant = this.timeInstant;
             return (timeInstant != null) ? timeInstant.timePosition : null;
         }
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/UniversalTimeAdapter.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/UniversalTimeAdapter.java
index 7379d4f..1c91065 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/UniversalTimeAdapter.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/UniversalTimeAdapter.java
@@ -42,7 +42,7 @@
     /**
      * The timezone of the date to marshal with this adapter.
      */
-    private static final TimeZone UTC = TimeZone.getTimeZone(org.apache.sis.util.privy.StandardDateFormat.UTC);
+    private static final TimeZone UTC = TimeZone.getTimeZone(org.apache.sis.util.privy.Constants.UTC);
 
     /**
      * Empty constructor for JAXB only.
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/privy/XmlUtilities.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/privy/XmlUtilities.java
index 8b5ea3c..0368141 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/privy/XmlUtilities.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/privy/XmlUtilities.java
@@ -192,22 +192,21 @@
      * @return the XML calendar, or {@code null} if {@code date} was null.
      * @throws DatatypeConfigurationException if the factory cannot be created.
      */
-    public static XMLGregorianCalendar toXML(final Context context, final Temporal date) throws DatatypeConfigurationException {
+    public static XMLGregorianCalendar toXML(final Context context, Temporal date) throws DatatypeConfigurationException {
         if (date == null) {
             return null;
         }
-        final XMLGregorianCalendar xml = getDatatypeFactory().newXMLGregorianCalendar();
         if (date instanceof Instant) {
             final TimeZone zone = (context != null) ? context.getTimeZone() : null;
             final ZoneId zid = (zone != null) ? zone.toZoneId() : ZoneId.systemDefault();
-            final ZonedDateTime t = ZonedDateTime.ofInstant((Instant) date, zid);
-            for (int i=0; i<FIELDS.length; i++) {
-                SETTERS[i].accept(xml, t.get(FIELDS[i]));
-            }
-        } else {
-            for (int i=0; i<FIELDS.length; i++) {
-                final ChronoField field = FIELDS[i];
-                if (date.isSupported(field)) {
+            date = ZonedDateTime.ofInstant((Instant) date, zid);
+        }
+        final XMLGregorianCalendar xml = getDatatypeFactory().newXMLGregorianCalendar();
+        for (int i=0; i<FIELDS.length; i++) {
+            final ChronoField field = FIELDS[i];
+            if (date.isSupported(field)) {
+                final int n = date.get(field);
+                if (n != 0 || field != ChronoField.MILLI_OF_SECOND) {
                     SETTERS[i].accept(xml, date.get(field));
                 }
             }
@@ -317,6 +316,20 @@
     }
 
     /**
+     * Converts the given XML Gregorian calendar to an instant.
+     * This method should be invoked only when the temporal object needs to be the {@link Instant} specialization.
+     * If the more generic {@link Temporal} type is okay, use {@link #toTemporal(Context, XMLGregorianCalendar)}.
+     *
+     * @param  context  the current (un)marshalling context, or {@code null} if none.
+     * @param  xml      the XML calendar to convert to a date, or {@code null}.
+     * @return the instant, or {@code null} if {@code xml} was null.
+     */
+    public static Instant toInstant(final Context context, final XMLGregorianCalendar xml) {
+        final Date date = toDate(context, xml);
+        return (date != null) ? date.toInstant() : null;
+    }
+
+    /**
      * Converts the given XML Gregorian calendar to a date.
      *
      * @param  context  the current (un)marshalling context, or {@code null} if none.
diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/extent/DefaultExtentTest.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/extent/DefaultExtentTest.java
index 9f0bcae..93c2582 100644
--- a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/extent/DefaultExtentTest.java
+++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/extent/DefaultExtentTest.java
@@ -17,6 +17,7 @@
 package org.apache.sis.metadata.iso.extent;
 
 import java.util.List;
+import java.time.Instant;
 import java.net.URL;
 import java.io.InputStream;
 import jakarta.xml.bind.JAXBException;
@@ -31,7 +32,6 @@
 import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.metadata.xml.TestUsingFile;
 import static org.apache.sis.metadata.Assertions.assertXmlEquals;
-import static org.apache.sis.test.TestUtilities.date;
 
 
 /**
@@ -73,13 +73,13 @@
      */
     @Test
     public void testIntersect() {
-        final DefaultGeographicBoundingBox bounds1   = new DefaultGeographicBoundingBox(10, 20, 30, 40);
-        final DefaultGeographicBoundingBox bounds2   = new DefaultGeographicBoundingBox(16, 18, 31, 42);
-        final DefaultGeographicBoundingBox clip      = new DefaultGeographicBoundingBox(15, 25, 26, 32);
-        final DefaultGeographicBoundingBox expected1 = new DefaultGeographicBoundingBox(15, 20, 30, 32);
-        final DefaultGeographicBoundingBox expected2 = new DefaultGeographicBoundingBox(16, 18, 31, 32);
-        final DefaultExtent e1 = new DefaultExtent("Somewhere", bounds1, null, null);
-        final DefaultExtent e2 = new DefaultExtent("Somewhere", clip, null, null);
+        final var bounds1   = new DefaultGeographicBoundingBox(10, 20, 30, 40);
+        final var bounds2   = new DefaultGeographicBoundingBox(16, 18, 31, 42);
+        final var clip      = new DefaultGeographicBoundingBox(15, 25, 26, 32);
+        final var expected1 = new DefaultGeographicBoundingBox(15, 20, 30, 32);
+        final var expected2 = new DefaultGeographicBoundingBox(16, 18, 31, 32);
+        final var e1 = new DefaultExtent("Somewhere", bounds1, null, null);
+        final var e2 = new DefaultExtent("Somewhere", clip, null, null);
         e1.getGeographicElements().add(bounds2);
         e1.intersect(e2);
         assertEquals("Somewhere", e1.getDescription().toString());
@@ -125,14 +125,15 @@
      * Compares the marshalling and unmarshalling of a {@link DefaultExtent} with XML in the given file.
      */
     private void roundtrip(final Format format) throws JAXBException {
-        final DefaultGeographicBoundingBox bbox = new DefaultGeographicBoundingBox(-99, -79, 14.9844, 31);
+        final var bbox = new DefaultGeographicBoundingBox(-99, -79, 14.9844, 31);
         bbox.getIdentifierMap().put(IdentifierSpace.ID, "bbox");
-        final DefaultTemporalExtent temporal = new DefaultTemporalExtent();
+        final var temporal = new DefaultTemporalExtent();
         if (PENDING_FUTURE_SIS_VERSION) {
             // This block needs a more complete sis-temporal module.
-            temporal.setBounds(date("2010-01-27 13:26:10"), date("2010-08-27 13:26:10"));
+            temporal.setBounds(Instant.parse("2010-01-27T13:26:10Z"),
+                               Instant.parse("2010-08-27T13:26:10Z"));
         }
-        final DefaultExtent extent = new DefaultExtent(null, bbox, null, temporal);
+        final var extent = new DefaultExtent(null, bbox, null, temporal);
         assertMarshalEqualsFile(openTestFile(format), extent, format.schemaVersion, "xmlns:*", "xsi:schemaLocation");
         assertEquals(extent, unmarshalFile(DefaultExtent.class, openTestFile(format)));
     }
diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/extent/DefaultTemporalExtentTest.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/extent/DefaultTemporalExtentTest.java
new file mode 100644
index 0000000..2ad7e3d
--- /dev/null
+++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/extent/DefaultTemporalExtentTest.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.metadata.iso.extent;
+
+import java.time.Instant;
+import org.opengis.referencing.operation.TransformException;
+
+// Test dependencies
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import org.apache.sis.test.TestCase;
+
+
+/**
+ * Tests {@link DefaultTemporalExtent}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class DefaultTemporalExtentTest extends TestCase {
+    /**
+     * Creates a new test case.
+     */
+    public DefaultTemporalExtentTest() {
+    }
+
+    /**
+     * Tests {@link DefaultTemporalExtent#intersect(TemporalExtent)}.
+     *
+     * @throws TransformException if the transformation failed.
+     */
+    @Test
+    public void testTemporalIntersection() throws TransformException {
+        final var e1 = new DefaultTemporalExtent();
+        final var e2 = new DefaultTemporalExtent();
+        final Instant t1 = Instant.parse("2016-12-05T19:45:20Z");
+        final Instant t2 = Instant.parse("2017-02-18T02:12:50Z");
+        final Instant t3 = Instant.parse("2017-11-30T23:50:00Z");
+        final Instant t4 = Instant.parse("2018-05-20T12:30:45Z");
+        e1.setBounds(t1, t3);
+        e2.setBounds(t2, t4);
+        e1.intersect(e2);
+        assertEquals(t2, e1.getBeginning().orElseThrow(), "beginning");
+        assertEquals(t3, e1.getEnding().orElseThrow(), "ending");
+    }
+}
diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/gml/TimePeriodTest.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/gml/TimePeriodTest.java
index 4699f27..68da733 100644
--- a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/gml/TimePeriodTest.java
+++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/gml/TimePeriodTest.java
@@ -36,7 +36,6 @@
 import org.apache.sis.xml.test.TestCase;
 import static org.apache.sis.metadata.Assertions.assertXmlEquals;
 import static org.apache.sis.test.TestUtilities.date;
-import static org.apache.sis.test.TestUtilities.format;
 
 
 /**
@@ -92,8 +91,8 @@
                 "  <gml:timePosition>1992-01-01T01:00:00.000+01:00</gml:timePosition>\n" +
                 "</gml:TimeInstant>\n", actual, "xmlns:*");
 
-        final TimeInstant test = (TimeInstant) unmarshal(unmarshaller, actual);
-        assertEquals("1992-01-01 00:00:00", format(XmlUtilities.toDate(context, test.timePosition)));
+        final var test = (TimeInstant) unmarshal(unmarshaller, actual);
+        assertEquals("1992-01-01T00:00:00Z", XmlUtilities.toInstant(context, test.timePosition).toString());
 
         pool.recycle(marshaller);
         pool.recycle(unmarshaller);
@@ -108,8 +107,8 @@
     @Test
     public void testPeriodGML2() throws JAXBException {
         createContext();
-        final TimePeriodBound begin = new TimePeriodBound.GML2(Instant.parse("1992-01-01T00:00:00Z"));
-        final TimePeriodBound end   = new TimePeriodBound.GML2(Instant.parse("2007-12-31T00:00:00Z"));
+        final var begin = new TimePeriodBound.GML2(Instant.parse("1992-01-01T00:00:00Z"));
+        final var end   = new TimePeriodBound.GML2(Instant.parse("2007-12-31T00:00:00Z"));
         testPeriod(begin, end,
                 "<gml:TimePeriod xmlns:gml=\"" + Namespaces.GML + "\">\n" +
                 "  <gml:begin>\n" +
@@ -142,10 +141,10 @@
         period.end   = end;
         final String actual = marshal(marshaller, period);
         assertXmlEquals(expected, actual, "xmlns:*");
-        final TimePeriod test = (TimePeriod) unmarshal(unmarshaller, actual);
+        final var test = (TimePeriod) unmarshal(unmarshaller, actual);
         if (verifyValues) {
-            assertEquals("1992-01-01 00:00:00", format(XmlUtilities.toDate(context, test.begin.calendar())));
-            assertEquals("2007-12-31 00:00:00", format(XmlUtilities.toDate(context, test.end  .calendar())));
+            assertEquals("1992-01-01T00:00:00Z", XmlUtilities.toInstant(context, test.begin.calendar()).toString());
+            assertEquals("2007-12-31T00:00:00Z", XmlUtilities.toInstant(context, test.end  .calendar()).toString());
         }
         pool.recycle(marshaller);
         pool.recycle(unmarshaller);
@@ -160,8 +159,8 @@
     @Test
     public void testPeriodGML3() throws JAXBException {
         createContext();
-        final TimePeriodBound begin = new TimePeriodBound.GML3(Instant.parse("1992-01-01T00:00:00Z"), "before");
-        final TimePeriodBound end   = new TimePeriodBound.GML3(Instant.parse("2007-12-31T00:00:00Z"), "after");
+        final var begin = new TimePeriodBound.GML3(Instant.parse("1992-01-01T00:00:00Z"), "before");
+        final var end   = new TimePeriodBound.GML3(Instant.parse("2007-12-31T00:00:00Z"), "after");
         testPeriod(begin, end,
                 "<gml:TimePeriod xmlns:gml=\"" + Namespaces.GML + "\">\n" +
                 "  <gml:beginPosition>1992-01-01T01:00:00+01:00</gml:beginPosition>\n" +
@@ -178,8 +177,8 @@
     @Test
     public void testSimplifiedPeriodGML3() throws JAXBException {
         createContext();
-        final TimePeriodBound begin = new TimePeriodBound.GML3(Instant.parse("1992-01-01T23:00:00Z"), "before");
-        final TimePeriodBound end   = new TimePeriodBound.GML3(Instant.parse("2007-12-30T23:00:00Z"), "after");
+        final var begin = new TimePeriodBound.GML3(Instant.parse("1992-01-01T23:00:00Z"), "before");
+        final var end   = new TimePeriodBound.GML3(Instant.parse("2007-12-30T23:00:00Z"), "after");
         testPeriod(begin, end,
                 "<gml:TimePeriod xmlns:gml=\"" + Namespaces.GML + "\">\n" +
                 "  <gml:beginPosition>1992-01-02</gml:beginPosition>\n" +
@@ -196,8 +195,8 @@
     @Test
     public void testBeforePeriodGML3() throws JAXBException {
         createContext();
-        final TimePeriodBound begin = new TimePeriodBound.GML3(null, "before");
-        final TimePeriodBound end   = new TimePeriodBound.GML3(Instant.parse("2007-12-30T23:00:00Z"), "after");
+        final var begin = new TimePeriodBound.GML3(null, "before");
+        final var end   = new TimePeriodBound.GML3(Instant.parse("2007-12-30T23:00:00Z"), "after");
         testPeriod(begin, end,
                 "<gml:TimePeriod xmlns:gml=\"" + Namespaces.GML + "\">\n" +
                 "  <gml:beginPosition indeterminatePosition=\"before\"/>\n" +
@@ -214,8 +213,8 @@
     @Test
     public void testAfterPeriodGML3() throws JAXBException {
         createContext();
-        final TimePeriodBound begin = new TimePeriodBound.GML3(Instant.parse("1992-01-01T23:00:00Z"), "before");
-        final TimePeriodBound end   = new TimePeriodBound.GML3(null, "after");
+        final var begin = new TimePeriodBound.GML3(Instant.parse("1992-01-01T23:00:00Z"), "before");
+        final var end   = new TimePeriodBound.GML3(null, "after");
         testPeriod(begin, end,
                 "<gml:TimePeriod xmlns:gml=\"" + Namespaces.GML + "\">\n" +
                 "  <gml:beginPosition>1992-01-02</gml:beginPosition>\n" +
diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/lan/LanguageCodeTest.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/lan/LanguageCodeTest.java
index bf2b96e..7249e95 100644
--- a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/lan/LanguageCodeTest.java
+++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/lan/LanguageCodeTest.java
@@ -27,7 +27,7 @@
 import org.apache.sis.xml.MarshallerPool;
 import org.apache.sis.xml.bind.cat.CodeListUID;
 import org.apache.sis.xml.privy.LegacyNamespaces;
-import static org.apache.sis.util.privy.StandardDateFormat.UTC;
+import static org.apache.sis.util.privy.Constants.UTC;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/privy/XmlUtilitiesTest.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/privy/XmlUtilitiesTest.java
index cc53d6f..2314f9f 100644
--- a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/privy/XmlUtilitiesTest.java
+++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/privy/XmlUtilitiesTest.java
@@ -61,10 +61,10 @@
     @Test
     public void testDateToXML() throws DatatypeConfigurationException, JAXBException {
         createContext(false, Locale.FRANCE, "CET");
-        final Date date = new Date(1230786000000L);
-        final XMLGregorianCalendar calendar = XmlUtilities.toXML(context, date);
+        final Instant date = Instant.ofEpochMilli(1230786000000L);
+        final XMLGregorianCalendar calendar = XmlUtilities.toXML(context, Date.from(date));
         assertEquals("2009-01-01T06:00:00.000+01:00", calendar.toString());
-        assertEquals(date, XmlUtilities.toDate(context, calendar));
+        assertEquals(date, XmlUtilities.toInstant(context, calendar));
 
         calendar.setMillisecond(FIELD_UNDEFINED);
         assertEquals("2009-01-01T06:00:00+01:00", calendar.toString());
@@ -85,21 +85,21 @@
 
         t = Instant.ofEpochMilli(1230786000000L);
         calendar = XmlUtilities.toXML(context, t);
-        assertEquals("2009-01-01T14:00:00.000+09:00", calendar.toString());
+        assertEquals("2009-01-01T14:00:00+09:00", calendar.toString());
 
         t = OffsetDateTime.parse("2009-01-01T06:00:00+01:00");
         calendar = XmlUtilities.toXML(context, t);
-        assertEquals("2009-01-01T06:00:00.000+01:00", calendar.toString());
+        assertEquals("2009-01-01T06:00:00+01:00", calendar.toString());
         assertEquals(t, XmlUtilities.toTemporal(context, calendar));
 
         t = LocalDateTime.parse("2009-08-12T06:20:10");
         calendar = XmlUtilities.toXML(context, t);
-        assertEquals("2009-08-12T06:20:10.000", calendar.toString());
+        assertEquals("2009-08-12T06:20:10", calendar.toString());
         assertEquals(t, XmlUtilities.toTemporal(context, calendar));
 
         t = LocalTime.parse("06:10:45");
         calendar = XmlUtilities.toXML(context, t);
-        assertEquals("06:10:45.000", calendar.toString());
+        assertEquals("06:10:45", calendar.toString());
         assertEquals(t, XmlUtilities.toTemporal(context, calendar));
 
         t = LocalDate.parse("2009-05-08");
diff --git a/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/AbstractLocation.java b/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/AbstractLocation.java
index ac8490e..59a73d2 100644
--- a/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/AbstractLocation.java
+++ b/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/AbstractLocation.java
@@ -22,7 +22,7 @@
 import org.opengis.metadata.extent.GeographicExtent;
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.geometry.Envelope;
-import org.opengis.geometry.coordinate.Position;
+import org.opengis.geometry.DirectPosition;
 import org.opengis.util.InternationalString;
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.geometry.Envelope2D;
@@ -196,13 +196,13 @@
      *
      * @return coordinates of a representative point for the location instance, or {@code null} if none.
      */
-    public Position getPosition() {
+    public DirectPosition getPosition() {
         final Envelope envelope = getEnvelope();
         if (envelope == null) {
             return null;
         }
         final int dimension = envelope.getDimension();
-        final GeneralDirectPosition pos = new GeneralDirectPosition(dimension);
+        final var pos = new GeneralDirectPosition(dimension);
         pos.setCoordinateReferenceSystem(envelope.getCoordinateReferenceSystem());
         for (int i=0; i<dimension; i++) {
             pos.setCoordinate(i, envelope.getMedian(i));
diff --git a/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/LocationFormat.java b/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/LocationFormat.java
index 6b41fd7..34494b1 100644
--- a/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/LocationFormat.java
+++ b/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/LocationFormat.java
@@ -27,6 +27,7 @@
 import java.text.NumberFormat;
 import java.text.ParseException;
 import java.text.ParsePosition;
+import java.time.Instant;
 import javax.measure.Unit;
 import org.opengis.util.FactoryException;
 import org.opengis.util.InternationalString;
@@ -38,7 +39,6 @@
 import org.opengis.metadata.extent.Extent;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
-import org.opengis.geometry.coordinate.Position;
 import org.apache.sis.io.TabularFormat;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.measure.UnitFormat;
@@ -171,13 +171,6 @@
     }
 
     /**
-     * Returns the direct position for the given position, or {@code null} if none.
-     */
-    private static DirectPosition position(final Position p) {
-        return (p != null) ? p.getDirectPosition() : null;
-    }
-
-    /**
      * Returns a localized version of the given international string, or {@code null} if none.
      */
     private static String toString(final InternationalString i18n, final Locale locale) {
@@ -187,8 +180,8 @@
     /**
      * Returns a localized version of the given date, or {@code null} if none.
      */
-    private String toString(final Date date) {
-        return (date != null) ? getFormat(Date.class).format(date) : null;
+    private String toString(final Instant date) {
+        return (date != null) ? getFormat(Date.class).format(Date.from(date)) : null;
     }
 
     /**
@@ -245,14 +238,14 @@
          * the axis order of the geographic bounding box.
          */
         final Extent extent = new DefaultExtent(null, location.getGeographicExtent(), null, location.getTemporalExtent());
-        final Range<Date> time = Extents.getTimeRange(extent);
+        final Range<Instant> time = Extents.getTimeRange(extent, null).orElse(null);
         if (time != null) {
             append(table, vocabulary, Vocabulary.Keys.StartDate, toString(time.getMinValue()));
             append(table, vocabulary, Vocabulary.Keys.EndDate,   toString(time.getMaxValue()));
         }
         GeographicBoundingBox     bbox     = Extents.getGeographicBoundingBox(extent);
         Envelope                  envelope = location.getEnvelope();
-        DirectPosition            position = position(location.getPosition());
+        DirectPosition            position = location.getPosition();
         DirectPosition            geopos   = null;                      // Position in geographic CRS.
         CoordinateReferenceSystem crs      = null;                      // Envelope Coordinate Reference System.
         CoordinateReferenceSystem normCRS  = null;                      // CRS in conventional (x,y) axis order.
diff --git a/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java b/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java
index c38a074..38a0e75 100644
--- a/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java
+++ b/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java
@@ -2217,7 +2217,7 @@
             if (!isValid) {
                 final String gzd;
                 try {
-                    gzd = owner.encoder(crs).encode(owner, getDirectPosition(), true, "", 0, 0);
+                    gzd = owner.encoder(crs).encode(owner, this, true, "", 0, 0);
                 } catch (IllegalArgumentException | FactoryException e) {
                     throw new GazetteerException(e.getLocalizedMessage(), e);
                 }
diff --git a/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/SimpleLocation.java b/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/SimpleLocation.java
index 19b6138..4672c25 100644
--- a/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/SimpleLocation.java
+++ b/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/SimpleLocation.java
@@ -18,7 +18,6 @@
 
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
-import org.opengis.geometry.coordinate.Position;
 import org.opengis.metadata.extent.GeographicExtent;
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.referencing.operation.MathTransform;
@@ -114,7 +113,7 @@
      * In this simple implementation, this instance is its own centroid coordinate.
      */
     @Override
-    public final Position getPosition() {
+    public final DirectPosition getPosition() {
         return this;
     }
 
diff --git a/endorsed/src/org.apache.sis.referencing.gazetteer/test/org/apache/sis/referencing/gazetteer/GeohashReferenceSystemTest.java b/endorsed/src/org.apache.sis.referencing.gazetteer/test/org/apache/sis/referencing/gazetteer/GeohashReferenceSystemTest.java
index 758af9a..e22fd23 100644
--- a/endorsed/src/org.apache.sis.referencing.gazetteer/test/org/apache/sis/referencing/gazetteer/GeohashReferenceSystemTest.java
+++ b/endorsed/src/org.apache.sis.referencing.gazetteer/test/org/apache/sis/referencing/gazetteer/GeohashReferenceSystemTest.java
@@ -250,7 +250,7 @@
     private void testDecode(final GeohashReferenceSystem.Coder coder, final int λi, final int φi) throws TransformException {
         for (final Place place : PLACES) {
             final AbstractLocation location = coder.decode(place.geohash);
-            final DirectPosition result = location.getPosition().getDirectPosition();
+            final DirectPosition result = location.getPosition();
             assertEquals(place.longitude, result.getOrdinate(λi), TOLERANCE, place.name);
             assertEquals(place.latitude,  result.getOrdinate(φi), TOLERANCE, place.name);
         }
diff --git a/endorsed/src/org.apache.sis.referencing.gazetteer/test/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystemTest.java b/endorsed/src/org.apache.sis.referencing.gazetteer/test/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystemTest.java
index 2360999..50b3000 100644
--- a/endorsed/src/org.apache.sis.referencing.gazetteer/test/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystemTest.java
+++ b/endorsed/src/org.apache.sis.referencing.gazetteer/test/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystemTest.java
@@ -214,7 +214,7 @@
     {
         final AbstractLocation loc = coder.decode(reference);
         final Envelope2D envelope = new Envelope2D(loc.getEnvelope());
-        final DirectPosition2D pos = new DirectPosition2D(loc.getPosition().getDirectPosition());
+        final DirectPosition2D pos = new DirectPosition2D(loc.getPosition());
         assertTrue(envelope.contains(pos), reference);
         return pos;
     }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/CoordinateFormat.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/CoordinateFormat.java
index 9c2e2e1..ade1c28 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/CoordinateFormat.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/CoordinateFormat.java
@@ -35,6 +35,8 @@
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.time.Instant;
 import javax.measure.Unit;
 import javax.measure.UnitConverter;
 import javax.measure.Quantity;
@@ -57,6 +59,8 @@
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.Characters;
 import org.apache.sis.util.privy.LocalizedParseException;
+import org.apache.sis.util.privy.TemporalDate;
+import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.Numerics;
 import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.math.MathFunctions;
@@ -71,6 +75,7 @@
 import org.apache.sis.measure.QuantityFormat;
 import org.apache.sis.measure.UnitFormat;
 import org.apache.sis.io.CompoundFormat;
+import org.apache.sis.pending.jdk.JDK23;
 
 
 /**
@@ -408,12 +413,12 @@
     private transient long negate;
 
     /**
-     * The time epochs. Non-null only if the at least on coordinate is to be formatted as a date.
+     * The time epochs. Non-null only if at least one coordinate is to be formatted as a date.
      *
      * <p>This array is created by {@link #createFormats(CoordinateReferenceSystem)}, which is invoked before
      * parsing or formatting in a different CRS than last operation, and stay unmodified after creation.</p>
      */
-    private transient long[] epochs;
+    private transient Instant[] epochs;
 
     /**
      * Dummy field position.
@@ -571,12 +576,12 @@
                 final CoordinateReferenceSystem t = CRS.getComponentAt(crs, i, i+1);
                 if (t instanceof TemporalCRS) {
                     if (epochs == null) {
-                        epochs = new long[dimension];
+                        epochs = new Instant[dimension];
                     }
                     types  [i] = DATE;
                     formats[i] = getFormat(Date.class);
-                    epochs [i] = ((TemporalCRS) t).getDatum().getOrigin().getTime();
-                    setConverter(dimension, i, unit.asType(Time.class).getConverterTo(Units.MILLISECOND));
+                    epochs [i] = TemporalDate.toInstant(((TemporalCRS) t).getDatum().getOrigin());
+                    setConverter(dimension, i, unit.asType(Time.class).getConverterTo(Units.SECOND));
                     if (direction == AxisDirection.PAST) {
                         negate(i);
                     }
@@ -1486,7 +1491,7 @@
                     case ANGLE:     valueObject = new Angle     (value); break;
                     case DATE: {
                         if (Double.isFinite(value)) {
-                            valueObject = new Date(Math.addExact(Math.round(value), epochs[i]));
+                            valueObject = TemporalDate.toDate(TemporalDate.addSeconds(epochs[i], value));
                         } else {
                             if (i != 0) toAppendTo.append(separator);
                             toAppendTo.append(String.valueOf(value));
@@ -1666,7 +1671,8 @@
             if (object instanceof Angle) {
                 value = ((Angle) object).degrees();
             } else if (object instanceof Date) {
-                value = Math.subtractExact(((Date) object).getTime(), epochs[i]);
+                final Duration d = JDK23.until(epochs[i], ((Date) object).toInstant());
+                value = d.getSeconds() + (d.getNano() / (double) Constants.NANOS_PER_SECOND);
             } else {
                 value = ((Number) object).doubleValue();
             }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java
index ff07101..d572f2d 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java
@@ -22,6 +22,7 @@
 import java.util.LinkedList;
 import java.util.Iterator;
 import java.util.Locale;
+import java.time.Instant;
 import java.text.ParsePosition;
 import java.text.ParseException;
 import org.opengis.referencing.cs.CoordinateSystem;
@@ -552,13 +553,13 @@
      * @return the next {@link Date} among the children.
      * @throws ParseException if no more date is available.
      */
-    public Date pullDate(final String key) throws ParseException {
+    public Instant pullDate(final String key) throws ParseException {
         final Iterator<Object> iterator = children.iterator();
         while (iterator.hasNext()) {
             final Object object = iterator.next();
             if (object instanceof Date) {
                 iterator.remove();
-                return (Date) object;
+                return ((Date) object).toInstant();
             }
         }
         throw missingComponent(key);
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
index 55c7bff..949957a 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
@@ -27,6 +27,8 @@
 import java.text.DateFormat;
 import java.text.NumberFormat;
 import java.text.FieldPosition;
+import java.time.Instant;
+import java.time.temporal.Temporal;
 import java.io.IOException;
 import java.lang.reflect.Array;
 import java.math.RoundingMode;
@@ -50,11 +52,10 @@
 import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.ConcatenatedOperation;
 import org.opengis.referencing.operation.MathTransform;
+import org.opengis.geometry.DirectPosition;
 import org.opengis.geometry.Envelope;
-import org.opengis.geometry.coordinate.Position;
 import org.apache.sis.measure.Units;
 import org.apache.sis.measure.UnitFormat;
-import org.apache.sis.measure.Range;
 import org.apache.sis.measure.MeasurementRange;
 import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.math.Vector;
@@ -925,7 +926,7 @@
             appendOnNewLine(WKTKeywords.Area, area.getDescription(), ElementKind.EXTENT);
             append(Extents.getGeographicBoundingBox(area), BBOX_ACCURACY);
             appendVerticalExtent(Extents.getVerticalRange(area));
-            appendTemporalExtent(Extents.getTimeRange(area));
+            appendTemporalExtent(area);
         }
     }
 
@@ -1005,10 +1006,10 @@
      *   <li>“{@code TemporalExtent[1980-04-12T18:00:00.0Z, 1980-04-12T21:00:00.0Z]}”</li>
      * </ul>
      */
-    private void appendTemporalExtent(final Range<Date> range) {
-        if (range != null) {
-            final Date min = range.getMinValue();
-            final Date max = range.getMaxValue();
+    private void appendTemporalExtent(final Extent area) {
+        Extents.getTimeRange(area, null).ifPresent((range) -> {
+            final Instant min = range.getMinValue();
+            final Instant max = range.getMaxValue();
             if (min != null && max != null) {
                 openElement(true, WKTKeywords.TimeExtent);
                 setColor(ElementKind.EXTENT);
@@ -1017,7 +1018,7 @@
                 resetColor();
                 closeElement(true);
             }
-        }
+        });
     }
 
     /**
@@ -1182,11 +1183,35 @@
     }
 
     /**
+     * Appends a temporal object (usually an instant).
+     * The {@linkplain Symbols#getSeparator() element separator} will be written before the date if needed.
+     *
+     * @param  date  the date to append to the WKT, or {@code null} if none.
+     *
+     * @since 1.5
+     */
+    public void append(final Temporal date) {
+        if (date != null) {
+            appendSeparator();
+            if (date instanceof Instant) {
+                // This is the usual case.
+                dateFormat.format(Date.from((Instant) date), buffer, dummy);
+            } else {
+                // Preserve the data structure (e.g. whether there is hours or not, timezone or not).
+                buffer.append(date);
+            }
+        }
+    }
+
+    /**
      * Appends a date.
      * The {@linkplain Symbols#getSeparator() element separator} will be written before the date if needed.
      *
      * @param  date  the date to append to the WKT, or {@code null} if none.
+     *
+     * @deprecated Replaced by {@link #append(Temporal)}.
      */
+    @Deprecated(since="1.5", forRemoval=true)
     public void append(final Date date) {
         if (date != null) {
             appendSeparator();
@@ -1195,10 +1220,10 @@
     }
 
     /**
-     * Appends a boolean value.
+     * Appends a Boolean value.
      * The {@linkplain Symbols#getSeparator() element separator} will be written before the boolean if needed.
      *
-     * @param  value  the boolean to append to the WKT.
+     * @param  value  the Boolean to append to the WKT.
      */
     public void append(final boolean value) {
         appendSeparator();
@@ -1572,6 +1597,7 @@
         }
         else if (value instanceof CodeList<?>) append((CodeList<?>) value);
         else if (value instanceof Date)        append((Date)        value);
+        else if (value instanceof Temporal)    append((Temporal)    value);
         else if (value instanceof Boolean)     append((Boolean)     value);
         else if (value instanceof CharSequence) {
             append((value instanceof InternationalString) ?
@@ -1632,9 +1658,9 @@
         } else if (value instanceof VerticalExtent) {
             appendVerticalExtent(Extents.getVerticalRange(new SimpleExtent(null, (VerticalExtent) value, null)));
         } else if (value instanceof TemporalExtent) {
-            appendTemporalExtent(Extents.getTimeRange(new SimpleExtent(null, null, (TemporalExtent) value)));
-        } else if (value instanceof Position) {
-            append(AbstractDirectPosition.castOrCopy(((Position) value).getDirectPosition()));
+            appendTemporalExtent(new SimpleExtent(null, null, (TemporalExtent) value));
+        } else if (value instanceof DirectPosition) {
+            append(AbstractDirectPosition.castOrCopy((DirectPosition) value));
         } else if (value instanceof Envelope) {
             append(AbstractEnvelope.castOrCopy((Envelope) value));          // Non-standard
         } else {
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
index ddd4ca6..30ab63b 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
@@ -26,13 +26,13 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.Date;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.text.DateFormat;
 import java.text.NumberFormat;
 import java.text.ParsePosition;
 import java.text.ParseException;
+import java.time.Instant;
 import static java.util.Collections.singletonMap;
 import javax.measure.Unit;
 import javax.measure.Quantity;
@@ -89,6 +89,7 @@
 
 // Specific to the main branch:
 import org.opengis.referencing.ReferenceSystem;
+import org.apache.sis.util.privy.TemporalDate;
 import org.apache.sis.referencing.internal.ServicesForMetadata;
 import org.apache.sis.referencing.factory.GeodeticObjectFactory;
 
@@ -542,8 +543,8 @@
                     element.close(ignoredElements);
                     warning(parent, element, Errors.formatInternational(Errors.Keys.UnsupportedType_1, "TimeExtent[String,String]"), null);
                 } else {
-                    final Date startTime = element.pullDate("startTime");
-                    final Date endTime   = element.pullDate("endTime");
+                    final Instant startTime = element.pullDate("startTime");
+                    final Instant endTime   = element.pullDate("endTime");
                     element.close(ignoredElements);
                     final DefaultTemporalExtent t = new DefaultTemporalExtent();
                     t.setBounds(startTime, endTime);
@@ -1495,11 +1496,11 @@
         }
         final String  name   = element.pullString ("name");
         final Element origin = element.pullElement(MANDATORY, WKTKeywords.TimeOrigin);
-        final Date    epoch  = origin .pullDate("origin");
+        final Instant epoch  = origin .pullDate("origin");
         origin.close(ignoredElements);
         final DatumFactory datumFactory = factories.getDatumFactory();
         try {
-            return datumFactory.createTemporalDatum(parseAnchorAndClose(element, name), epoch);
+            return datumFactory.createTemporalDatum(parseAnchorAndClose(element, name), TemporalDate.toDate(epoch));
         } catch (FactoryException exception) {
             throw element.parseFailed(exception);
         }
@@ -2056,9 +2057,9 @@
          * A ParametricCRS can be either a "normal" one (with a non-null datum), or a DerivedCRS of kind ParametricCRS.
          * In the latter case, the datum is null and we have instead DerivingConversion element from a BaseParametricCRS.
          */
-        Datum           datum    = null;
-        SingleCRS       baseCRS  = null;
-        Conversion      fromBase = null;
+        Datum datum = null;
+        SingleCRS baseCRS = null;
+        Conversion fromBase = null;
         if (!isBaseCRS) {
             /*
              * UNIT[…] in DerivedCRS parameters are mandatory according ISO 19162 and the specification does not said
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/WKTFormat.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/WKTFormat.java
index 1949a46..01a1e8d 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/WKTFormat.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/WKTFormat.java
@@ -1066,7 +1066,7 @@
      * {@link org.opengis.metadata.extent.VerticalExtent},
      * {@link org.opengis.metadata.extent.TemporalExtent},
      * {@link org.opengis.geometry.Envelope},
-     * {@link org.opengis.geometry.coordinate.Position}
+     * {@link org.opengis.geometry.DirectPosition}
      * and {@link Unit}.
      *
      * @param  object      the object to format.
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
index 45db156..62c37c6 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
@@ -84,7 +84,7 @@
 import org.apache.sis.math.MathFunctions;
 import org.apache.sis.measure.Latitude;
 import org.apache.sis.measure.Units;
-import static org.apache.sis.util.privy.StandardDateFormat.MILLISECONDS_PER_DAY;
+import static org.apache.sis.util.privy.Constants.SECONDS_PER_DAY;
 
 // Specific to the main branch:
 import org.opengis.referencing.crs.GeocentricCRS;
@@ -1557,13 +1557,13 @@
          * the legacy date/time formatting classes in the {@link java.text} package uses the proleptic
          * Julian calendar for dates before October 15, 1582, while the new date/time formatting classes
          * in the {@link java.time.format} package use the ISO-8601 calendar system, which is equivalent
-         * to the proleptic Gregorian calendar for every dates. For parsing and formatting of Julian days,
-         * the {@link java.text.SimpleDateFormat} class is closer to the common practice (but not ISO 8601
-         * compliant).</p>
+         * to the proleptic <em>Gregorian</em> calendar for every dates. For parsing and formatting of
+         * Julian days, the {@link java.text.SimpleDateFormat} class is closer to the common practice
+         * (but not ISO 8601 compliant).</p>
          *
          * @see <a href="https://en.wikipedia.org/wiki/Julian_day">Julian day on Wikipedia</a>
          */
-        JULIAN(Vocabulary.Keys.Julian, -2440588L * MILLISECONDS_PER_DAY + MILLISECONDS_PER_DAY/2,
+        JULIAN(Vocabulary.Keys.Julian, -2440588L * SECONDS_PER_DAY + SECONDS_PER_DAY/2,
                "JulianDate", true),
 
         /**
@@ -1574,7 +1574,7 @@
          *
          * @see <a href="https://en.wikipedia.org/wiki/Julian_day">Julian day on Wikipedia</a>
          */
-        MODIFIED_JULIAN(Vocabulary.Keys.ModifiedJulian, -40587L * MILLISECONDS_PER_DAY,
+        MODIFIED_JULIAN(Vocabulary.Keys.ModifiedJulian, -40587L * SECONDS_PER_DAY,
                         "ModifiedJulianDate", false),
 
         /**
@@ -1586,7 +1586,7 @@
          *
          * @see <a href="https://en.wikipedia.org/wiki/Julian_day">Julian day on Wikipedia</a>
          */
-        TRUNCATED_JULIAN(Vocabulary.Keys.TruncatedJulian, -587L * MILLISECONDS_PER_DAY,
+        TRUNCATED_JULIAN(Vocabulary.Keys.TruncatedJulian, -587L * SECONDS_PER_DAY,
                          "TruncatedJulianDate", true),
 
         /**
@@ -1597,7 +1597,7 @@
          *
          * @see <a href="https://en.wikipedia.org/wiki/Julian_day">Julian day on Wikipedia</a>
          */
-        DUBLIN_JULIAN(Vocabulary.Keys.DublinJulian, -25568L * MILLISECONDS_PER_DAY + MILLISECONDS_PER_DAY/2,
+        DUBLIN_JULIAN(Vocabulary.Keys.DublinJulian, -25568L * SECONDS_PER_DAY + SECONDS_PER_DAY/2,
                       "DublinJulian", false),
 
         /**
@@ -1615,7 +1615,7 @@
          *
          * @since 1.5
          */
-        TROPICAL_YEAR(Vocabulary.Keys.TropicalYear, 946684800000L, "TropicalYear", false),
+        TROPICAL_YEAR(Vocabulary.Keys.TropicalYear, 946684800L, "TropicalYear", false),
 
         /**
          * Time measured as seconds since January 1st, 1970 at 00:00 UTC.
@@ -1633,7 +1633,7 @@
         private final short key;
 
         /**
-         * The date and time origin of this temporal datum.
+         * The date and time origin of this temporal datum in seconds since January 1st, 1970 at 00:00 UTC.
          */
         private final long epoch;
 
@@ -1727,8 +1727,8 @@
          */
         @OptionalCandidate
         public static Temporal forEpoch(final Instant epoch) {
-            if (epoch != null) {
-                final long e = epoch.toEpochMilli();
+            if (epoch != null && epoch.getNano() == 0) {
+                final long e = epoch.getEpochSecond();
                 for (final Temporal candidate : values()) {
                     if (candidate.epoch == e) {
                         return candidate;
@@ -1847,7 +1847,7 @@
                         } else {
                             properties = properties(key);
                         }
-                        object = new DefaultTemporalDatum(properties, new Date(epoch));
+                        object = new DefaultTemporalDatum(properties, Instant.ofEpochSecond(epoch));
                         cached = object;
                     }
                 }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/GeodesicsOnEllipsoid.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/GeodesicsOnEllipsoid.java
index f131d5b..206a848 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/GeodesicsOnEllipsoid.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/GeodesicsOnEllipsoid.java
@@ -17,7 +17,7 @@
 package org.apache.sis.referencing;
 
 import static java.lang.Math.*;
-import org.opengis.geometry.coordinate.Position;
+import org.opengis.geometry.DirectPosition;
 import org.opengis.referencing.datum.Ellipsoid;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.referencing.internal.Resources;
@@ -209,7 +209,7 @@
     /**
      * Constructs a new geodetic calculator expecting coordinates in the supplied CRS.
      *
-     * @param  crs         the referencing system for the {@link Position} arguments and return values.
+     * @param  crs         the referencing system for the {@link DirectPosition} arguments and return values.
      * @param  ellipsoid   ellipsoid associated to the geodetic component of given CRS.
      */
     GeodesicsOnEllipsoid(final CoordinateReferenceSystem crs, final Ellipsoid ellipsoid) {
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/GeodeticCalculator.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/GeodeticCalculator.java
index 9b7c01e..a490213 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/GeodeticCalculator.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/GeodeticCalculator.java
@@ -35,7 +35,6 @@
 import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.geometry.DirectPosition;
-import org.opengis.geometry.coordinate.Position;
 import org.apache.sis.measure.AngleFormat;
 import org.apache.sis.measure.Latitude;
 import org.apache.sis.measure.Units;
@@ -70,11 +69,11 @@
  *
  * <p>This class uses the following information:</p>
  * <ul>
- *   <li>The {@linkplain #setStartPoint(Position) start point}, which is always considered valid after the first call
+ *   <li>The {@linkplain #setStartPoint(DirectPosition) start point}, which is always considered valid after the first call
  *     to {@code setStartPoint(…)}. Its value can only be changed by another call to {@code setStartPoint(…)}.</li>
  *   <li>One of the followings (the latest specified properties override other properties and determines what will be calculated):
  *     <ul>
- *       <li>the {@linkplain #setEndPoint(Position) end point}, or</li>
+ *       <li>the {@linkplain #setEndPoint(DirectPosition) end point}, or</li>
  *       <li>the {@linkplain #setStartingAzimuth(double) azimuth at start point} together with
  *           the {@linkplain #setGeodesicDistance(double) geodesic distance} from that point.</li>
  *     </ul>
@@ -153,7 +152,7 @@
      *
      * <ul>
      *   <li>{@link PositionTransformer#defaultCRS} is the default CRS for all methods receiving a
-     *       {@link Position} argument if the given position does not specify its own CRS.</li>
+     *       {@link DirectPosition} argument if the given position does not specify its own CRS.</li>
      *   <li>{@link PositionTransformer#getCoordinateReferenceSystem()} is the CRS of all methods
      *       receiving (φ,λ) arguments as {@code double} values.</li>
      * </ul>
@@ -269,7 +268,7 @@
      * <p>This class is currently not designed for sub-classing outside this package. If in a future version we want to
      * relax this restriction, we should revisit the package-private API in order to commit to a safer protected API.</p>
      *
-     * @param  crs         the reference system for the {@link Position} arguments and return values.
+     * @param  crs         the reference system for the {@link DirectPosition} arguments and return values.
      * @param  ellipsoid   ellipsoid associated to the geodetic component of given CRS.
      */
     GeodeticCalculator(final CoordinateReferenceSystem crs, final Ellipsoid ellipsoid) {
@@ -285,11 +284,11 @@
 
     /**
      * Constructs a new geodetic calculator expecting coordinates in the supplied CRS.
-     * All {@code GeodeticCalculator} methods having a {@link Position} argument
+     * All {@code GeodeticCalculator} methods having a {@link DirectPosition} argument
      * or return value will use that specified CRS.
      * That CRS is the value returned by {@link #getPositionCRS()}.
      *
-     * @param  crs  the reference system for the {@link Position} objects.
+     * @param  crs  the reference system for the {@link DirectPosition} objects.
      * @return a new geodetic calculator using the specified CRS.
      */
     public static GeodeticCalculator create(final CoordinateReferenceSystem crs) {
@@ -320,13 +319,14 @@
     }
 
     /**
-     * Returns the Coordinate Reference System (CRS) in which {@code Position}s are represented, unless otherwise specified.
-     * This is the CRS of all {@link Position} instances returned by methods in this class. This is also the default CRS
-     * assumed by methods receiving a {@link Position} argument when the given position does not specify its CRS.
+     * Returns the Coordinate Reference System (CRS) in which positions are represented, unless otherwise specified.
+     * This is the CRS of all {@link DirectPosition} instances returned by methods in this class.
+     * This is also the default CRS assumed by methods receiving a {@link DirectPosition} argument
+     * when the given position does not specify its CRS.
      * This default CRS is specified at construction time.
      * It is not necessarily geographic; it may be projected or geocentric.
      *
-     * @return the default CRS for {@link Position} instances.
+     * @return the default CRS for {@link DirectPosition} instances.
      */
     public CoordinateReferenceSystem getPositionCRS() {
         return userToGeodetic.defaultCRS;
@@ -400,12 +400,12 @@
      * @param  point  the starting point in any coordinate reference system.
      * @throws IllegalArgumentException if the given coordinates cannot be transformed.
      *
-     * @see #setEndPoint(Position)
+     * @see #setEndPoint(DirectPosition)
      */
-    public void setStartPoint(final Position point) {
+    public void setStartPoint(final DirectPosition point) {
         final DirectPosition p;
         try {
-            p = userToGeodetic.transform(point.getDirectPosition());
+            p = userToGeodetic.transform(point);
         } catch (TransformException e) {
             throw new IllegalArgumentException(transformError(false), e);
         }
@@ -435,7 +435,7 @@
 
     /**
      * Returns or computes the destination in the CRS specified at construction time. This method returns
-     * the point specified in the last call to a {@link #setEndPoint(Position) setEndPoint(…)} method,
+     * the point specified in the last call to a {@link #setEndPoint(DirectPosition) setEndPoint(…)} method,
      * unless the {@linkplain #setStartingAzimuth(double) starting azimuth} and
      * {@linkplain #setGeodesicDistance(double) geodesic distance} have been set more recently.
      * In the latter case, the end point will be computed from the {@linkplain #getStartPoint() start point}
@@ -467,12 +467,12 @@
      * @param  position  the destination (end point) in any coordinate reference system.
      * @throws IllegalArgumentException if the given coordinates cannot be transformed.
      *
-     * @see #setStartPoint(Position)
+     * @see #setStartPoint(DirectPosition)
      */
-    public void setEndPoint(final Position position) {
+    public void setEndPoint(final DirectPosition position) {
         final DirectPosition p;
         try {
-            p = userToGeodetic.transform(position.getDirectPosition());
+            p = userToGeodetic.transform(position);
         } catch (TransformException e) {
             throw new IllegalArgumentException(transformError(false), e);
         }
@@ -504,7 +504,7 @@
      * Returns or computes the angular heading at the starting point of a geodesic path.
      * Azimuth is relative to geographic North with values increasing clockwise.
      * This method returns the azimuth normalized to [-180 … +180]° range given in last call to
-     * {@link #setStartingAzimuth(double)} method, unless the {@link #setEndPoint(Position) setEndPoint(…)}
+     * {@link #setStartingAzimuth(double)} method, unless the {@link #setEndPoint(DirectPosition) setEndPoint(…)}
      * method has been invoked more recently. In the latter case, the azimuth will be computed from the
      * {@linkplain #getStartPoint() start point} and the current end point.
      *
@@ -541,8 +541,9 @@
 
     /**
      * Computes the angular heading at the ending point of a geodesic path.
-     * Azimuth is relative to geographic North with values increasing clockwise. This method computes the azimuth
-     * from the current {@linkplain #setStartPoint(Position) start point} and {@linkplain #setEndPoint(Position) end point},
+     * Azimuth is relative to geographic North with values increasing clockwise.
+     * This method computes the azimuth from the current {@linkplain #setStartPoint(DirectPosition) start point}
+     * and {@linkplain #setEndPoint(DirectPosition) end point},
      * or from start point and the current {@linkplain #setStartingAzimuth(double) starting azimuth} and
      * {@linkplain #setGeodesicDistance(double) geodesic distance}.
      *
@@ -579,8 +580,9 @@
     /**
      * Returns or computes the shortest distance from start point to end point. This is sometimes called "great circle"
      * or "orthodromic" distance. This method returns the value given in last call to {@link #setGeodesicDistance(double)},
-     * unless the {@link #setEndPoint(Position) setEndPoint(…)} method has been invoked more recently. In the latter case,
-     * the distance will be computed from the {@linkplain #getStartPoint() start point} and current end point.
+     * unless the {@link #setEndPoint(DirectPosition) setEndPoint(…)} method has been invoked more recently.
+     * In the latter case, the distance will be computed from the {@linkplain #getStartPoint() start point}
+     * and current end point.
      *
      * @return the shortest distance in the unit of measurement given by {@link #getDistanceUnit()}.
      * @throws IllegalStateException if the start point or end point has not been set.
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultTemporalCRS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultTemporalCRS.java
index 6f95f9b..b9c9390 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultTemporalCRS.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultTemporalCRS.java
@@ -21,6 +21,8 @@
 import java.util.Objects;
 import java.time.Instant;
 import java.time.Duration;
+import java.time.temporal.Temporal;
+import java.time.temporal.ChronoField;
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import jakarta.xml.bind.annotation.XmlType;
@@ -40,8 +42,11 @@
 import org.apache.sis.io.wkt.Formatter;
 import org.apache.sis.measure.Units;
 import org.apache.sis.math.Fraction;
-import static org.apache.sis.util.privy.StandardDateFormat.NANOS_PER_SECOND;
-import static org.apache.sis.util.privy.StandardDateFormat.MILLIS_PER_SECOND;
+import static org.apache.sis.util.privy.Constants.NANOS_PER_SECOND;
+import static org.apache.sis.util.privy.Constants.MILLIS_PER_SECOND;
+
+// Specific to the main and geoapi-3.1 branches:
+import org.apache.sis.util.privy.TemporalDate;
 
 
 /**
@@ -223,16 +228,16 @@
      */
     private void initializeConverter() {
         toSeconds = getUnit().getConverterTo(Units.SECOND);
-        long t = datum.getOrigin().getTime();
-        origin = t / MILLIS_PER_SECOND;
-        t %= MILLIS_PER_SECOND;
-        if (t != 0) {
+        final Temporal t = TemporalDate.toTemporal(datum.getOrigin());
+        origin = t.getLong(ChronoField.INSTANT_SECONDS);
+        int r = t.get(ChronoField.NANO_OF_SECOND);
+        if (r != 0) {
             /*
              * The origin is usually an integer number of days or hours. It rarely has a fractional number of seconds.
              * If it happens anyway, put the fractional number of seconds in the converter instead of adding another
              * field in this class for such very rare situation. Accuracy should be okay since the offset is small.
              */
-            UnitConverter c = Units.converter(null, Fraction.valueOf(t, MILLIS_PER_SECOND));
+            UnitConverter c = Units.converter(null, new Fraction(r, NANOS_PER_SECOND));
             toSeconds = c.concatenate(toSeconds);
         }
     }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/BursaWolfParameters.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/BursaWolfParameters.java
index 637b7a0..759856a 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/BursaWolfParameters.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/BursaWolfParameters.java
@@ -16,9 +16,9 @@
  */
 package org.apache.sis.referencing.datum;
 
-import java.util.Date;
 import java.util.Arrays;
 import java.util.Objects;
+import java.time.temporal.Temporal;
 import java.io.Serializable;
 import static java.lang.Math.abs;
 import org.opengis.metadata.extent.Extent;
@@ -120,7 +120,7 @@
  * (case 1 above) over the <i>early-binding</i> approach (case 3 above).
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @see DefaultGeodeticDatum#getBursaWolfParameters()
  * @see <a href="https://en.wikipedia.org/wiki/Helmert_transformation">Wikipedia: Helmert transformation</a>
@@ -386,7 +386,7 @@
 
     /**
      * Inverts in-place the transformation by inverting the sign of all numerical parameters.
-     * The {@linkplain #getPositionVectorTransformation(Date) position vector transformation} matrix
+     * The {@linkplain #getPositionVectorTransformation(Temporal) position vector transformation} matrix
      * created from inverted Bursa-Wolf parameters will be <strong>approximately</strong> equals
      * to the {@linkplain org.apache.sis.referencing.operation.matrix.MatrixSIS#inverse() inverse}
      * of the matrix created from the original parameters. The equality holds approximately only
@@ -406,7 +406,7 @@
      *
      * @return fractional number of tropical years since reference time, or {@code null}.
      */
-    DoubleDouble period(final Date time) {
+    DoubleDouble period(final Temporal time) {
         return null;
     }
 
@@ -414,7 +414,7 @@
      * Returns the parameter at the given index. If this {@code BursaWolfParameters} is time-dependent,
      * then the returned value shall be corrected the time elapsed since the reference time.
      *
-     * The {@code factor} argument shall be the value computed by {@link #period(Date)},
+     * The {@code factor} argument shall be the value computed by {@link #period(Temporal)},
      * multiplied by 1000 for all {@code index} values except 6.
      * The 1000 factor is for conversion mm/year to m/year or milli-arc-seconds to arc-seconds.
      *
@@ -478,8 +478,10 @@
      * @return an affine transform in geocentric space created from this Bursa-Wolf parameters and the given time.
      *
      * @see DefaultGeodeticDatum#getPositionVectorTransformation(GeodeticDatum, Extent)
+     *
+     * @since 1.5
      */
-    public Matrix getPositionVectorTransformation(final Date time) {
+    public Matrix getPositionVectorTransformation(final Temporal time) {
         final DoubleDouble period = period(time);
         if (period == null && isTranslation()) {
             final Matrix4 matrix = new Matrix4();
@@ -524,7 +526,7 @@
      * @param  tolerance  the tolerance error for the skew-symmetric matrix test, in units of PPM or arc-seconds (e.g. 1E-8).
      * @throws IllegalArgumentException if the specified matrix does not met the conditions.
      *
-     * @see #getPositionVectorTransformation(Date)
+     * @see #getPositionVectorTransformation(Temporal)
      */
     public void setPositionVectorTransformation(final Matrix matrix, final double tolerance) throws IllegalArgumentException {
         final int numRow = matrix.getNumRow();
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java
index 1ec57ec..9dc62de 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java
@@ -18,7 +18,6 @@
 
 import java.util.Map;
 import java.util.Arrays;
-import java.util.Date;
 import java.util.Objects;
 import java.util.logging.Logger;
 import java.time.Instant;
@@ -413,7 +412,7 @@
      * @param  areaOfInterest  the geographic and temporal extent where the transformation should be valid, or {@code null}.
      * @return an affine transform from {@code this} to {@code target} in geocentric space, or {@code null} if none.
      *
-     * @see BursaWolfParameters#getPositionVectorTransformation(Date)
+     * @see BursaWolfParameters#getPositionVectorTransformation(Temporal)
      */
     public Matrix getPositionVectorTransformation(final GeodeticDatum targetDatum, final Extent areaOfInterest) {
         ensureNonNull("targetDatum", targetDatum);
@@ -432,7 +431,7 @@
                 return Matrices.inverse(createTransformation(candidate, areaOfInterest));
             } catch (NoninvertibleMatrixException e) {
                 /*
-                 * Should never happen because BursaWolfParameters.getPositionVectorTransformation(Date)
+                 * Should never happen because BursaWolfParameters.getPositionVectorTransformation(Temporal)
                  * is defined in such a way that matrix should always be invertible. If it happen anyway,
                  * returning `null` is allowed by this method's contract.
                  */
@@ -483,7 +482,7 @@
     }
 
     /**
-     * Invokes {@link BursaWolfParameters#getPositionVectorTransformation(Date)} for a date calculated from
+     * Invokes {@link BursaWolfParameters#getPositionVectorTransformation(Temporal)} for a date calculated from
      * the temporal elements on the given extent. This method chooses an instant located midway between the
      * start and end time.
      */
@@ -493,7 +492,8 @@
          * not a subclass of BursaWolfParameters. This optimisation covers the vast majority of cases.
          */
         return bursaWolf.getPositionVectorTransformation(bursaWolf.getClass() != BursaWolfParameters.class ?
-                Extents.getDate(areaOfInterest, 0.5) : null);       // 0.5 is for choosing midway instant.
+                Extents.getInstant(areaOfInterest, null, 0.5).orElse(null) : null);
+                // 0.5 is for choosing the instant midway between start and end.
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultTemporalDatum.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultTemporalDatum.java
index f7aade2..5f95872 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultTemporalDatum.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultTemporalDatum.java
@@ -19,6 +19,7 @@
 import java.util.Date;
 import java.util.Map;
 import java.util.Objects;
+import java.time.temporal.Temporal;
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlSchemaType;
 import jakarta.xml.bind.annotation.XmlElement;
@@ -28,8 +29,7 @@
 import org.opengis.util.InternationalString;
 import org.opengis.referencing.datum.TemporalDatum;
 import org.apache.sis.referencing.privy.WKTKeywords;
-import org.apache.sis.xml.bind.gml.UniversalTimeAdapter;
-import org.apache.sis.metadata.privy.ImplementationHelper;
+import org.apache.sis.xml.bind.gml.TemporalAdapter;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.io.wkt.Formatter;
 import org.apache.sis.io.wkt.FormattableObject;
@@ -37,6 +37,9 @@
 // Specific to the main branch:
 import org.opengis.referencing.ReferenceIdentifier;
 
+// Specific to the main and geoapi-3.1 branches:
+import org.apache.sis.util.privy.TemporalDate;
+
 
 /**
  * Defines the origin of a temporal coordinate reference system.
@@ -70,7 +73,7 @@
  * all components were created using only SIS factories and static constants.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @see org.apache.sis.referencing.CommonCRS.Temporal#datum()
  * @see org.apache.sis.referencing.cs.DefaultTimeCS
@@ -85,17 +88,21 @@
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = 3357241732140076884L;
+    private static final long serialVersionUID = -1507650596130032757L;
 
     /**
-     * The date and time origin of this temporal datum, or {@link Long#MIN_VALUE} if none.
-     * This information is mandatory, but SIS is tolerant to missing value is case a XML
-     * fragment was incomplete.
+     * The date and time origin of this temporal datum, or {@code null} if none.
+     * This information is mandatory, but SIS is tolerant to missing value
+     * is case a XML fragment was incomplete.
      *
      * <p><b>Consider this field as final!</b>
-     * This field is modified only at unmarshalling time by {@link #setOrigin(Date)}</p>
+     * This field is modified only at unmarshalling time.</p>
      */
-    private long origin;
+    @SuppressWarnings("serial")         // Most implementations are serializable.
+    @XmlSchemaType(name = "dateTime")
+    @XmlElement(name = "origin", required = true)
+    @XmlJavaTypeAdapter(TemporalAdapter.class)
+    private Temporal origin;
 
     /**
      * Creates a temporal datum from the given properties. The properties map is given
@@ -143,10 +150,25 @@
      * @param  origin      the date and time origin of this temporal datum.
      *
      * @see org.apache.sis.referencing.factory.GeodeticObjectFactory#createTemporalDatum(Map, Date)
+     *
+     * @since 1.5
      */
-    public DefaultTemporalDatum(final Map<String,?> properties, final Date origin) {
+    public DefaultTemporalDatum(final Map<String,?> properties, final Temporal origin) {
         super(properties);
-        this.origin = origin.getTime();
+        this.origin = origin;
+    }
+
+    /**
+     * Creates a temporal datum from the given properties.
+     *
+     * @param  properties  the properties to be given to the identified object.
+     * @param  origin      the date and time origin of this temporal datum.
+     *
+     * @deprecated Use {@link #DefaultTemporalDatum(Map, Temporal)} instead.
+     */
+    @Deprecated(since="1.5")
+    public DefaultTemporalDatum(final Map<String,?> properties, final Date origin) {
+        this(properties, TemporalDate.toTemporal(origin));
     }
 
     /**
@@ -162,7 +184,7 @@
      */
     protected DefaultTemporalDatum(final TemporalDatum datum) {
         super(datum);
-        origin = ImplementationHelper.toMilliseconds(datum.getOrigin());
+        origin = TemporalDate.toTemporal(datum.getOrigin());
     }
 
     /**
@@ -202,11 +224,8 @@
      * @return the date and time origin of this temporal datum.
      */
     @Override
-    @XmlSchemaType(name = "dateTime")
-    @XmlElement(name = "origin", required = true)
-    @XmlJavaTypeAdapter(UniversalTimeAdapter.class)
     public Date getOrigin() {
-        return ImplementationHelper.toDate(origin);
+        return TemporalDate.toDate(origin);
     }
 
     /**
@@ -228,7 +247,7 @@
         }
         switch (mode) {
             case STRICT: {
-                return origin == ((DefaultTemporalDatum) object).origin;
+                return Objects.equals(origin, ((DefaultTemporalDatum) object).origin);
             }
             default: {
                 return Objects.equals(getOrigin(), ((TemporalDatum) object).getOrigin());
@@ -245,7 +264,7 @@
      */
     @Override
     protected long computeHashCode() {
-        return super.computeHashCode() + origin;
+        return super.computeHashCode() + Objects.hashCode(origin);
     }
 
     /**
@@ -261,7 +280,7 @@
     @Override
     protected String formatTo(final Formatter formatter) {
         super.formatTo(formatter);
-        formatter.append(new Origin(getOrigin()));
+        formatter.append(new Origin(TemporalDate.toTemporal(getOrigin())));
         if (formatter.getConvention().majorVersion() == 1) {
             formatter.setInvalidWKT(this, null);
         }
@@ -273,10 +292,10 @@
      */
     private static final class Origin extends FormattableObject {
         /** The value of the origin to format. */
-        private final Date origin;
+        private final Temporal origin;
 
         /** Creates a new time origin with the given value. */
-        Origin(final Date origin) {
+        Origin(final Temporal origin) {
             this.origin = origin;
         }
 
@@ -310,24 +329,10 @@
      * reserved to JAXB, which will assign values to the fields using reflection.
      */
     private DefaultTemporalDatum() {
-        origin = Long.MIN_VALUE;
         /*
          * The origin is mandatory for SIS working. We do not verify its presence here because the verification
-         * would have to be done in an 'afterMarshal(…)' method and throwing an exception in that method causes
+         * would have to be done in an `afterMarshal(…)` method and throwing an exception in that method causes
          * the whole unmarshalling to fail. But the CD_TemporalDatum adapter does some verifications.
          */
     }
-
-    /**
-     * Invoked by JAXB only at unmarshalling time.
-     *
-     * @see #getOrigin()
-     */
-    private void setOrigin(final Date value) {
-        if (origin == Long.MIN_VALUE) {
-            origin = value.getTime();
-        } else {
-            ImplementationHelper.propertyAlreadySet(DefaultTemporalDatum.class, "setOrigin", "origin");
-        }
-    }
 }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/TimeDependentBWP.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/TimeDependentBWP.java
index f0cfaa9..a92486d 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/TimeDependentBWP.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/TimeDependentBWP.java
@@ -17,12 +17,15 @@
 package org.apache.sis.referencing.datum;
 
 import java.util.Date;
+import java.util.Objects;
+import java.time.Duration;
+import java.time.temporal.Temporal;
 import org.opengis.metadata.extent.Extent;
 import org.opengis.referencing.datum.GeodeticDatum;
 import org.opengis.referencing.datum.PrimeMeridian;
 import org.apache.sis.util.privy.DoubleDouble;
+import org.apache.sis.util.privy.Constants;
 import static org.apache.sis.util.ArgumentChecks.*;
-import static org.apache.sis.referencing.privy.Formulas.JULIAN_YEAR_LENGTH;
 
 
 /**
@@ -57,7 +60,7 @@
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  * @since   0.4
  */
 @SuppressWarnings("CloneableImplementsClone")                   // Fields in this class do not need cloning.
@@ -65,7 +68,7 @@
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = -4628799278259080258L;
+    private static final long serialVersionUID = 8404372938646871943L;
 
     /**
      * Rate of change of X-axis translation in millimetres per year (EPSG:1040).
@@ -108,7 +111,8 @@
     /**
      * The reference epoch for time-dependent parameters (EPSG:1047).
      */
-    private final long timeReference;
+    @SuppressWarnings("serial")         // Most implementations are serializable.
+    private final Temporal timeReference;
 
     /**
      * Creates a new instance for the given target datum, domain of validity and time reference.
@@ -119,10 +123,22 @@
      * @param domainOfValidity  area or region in which a coordinate transformation based on those Bursa-Wolf parameters
      *                          is valid, or {@code null} if unspecified.
      * @param timeReference     the reference epoch for time-dependent parameters.
+     *
+     * @since 1.5
      */
-    public TimeDependentBWP(final GeodeticDatum targetDatum, final Extent domainOfValidity, final Date timeReference) {
+    public TimeDependentBWP(final GeodeticDatum targetDatum, final Extent domainOfValidity, final Temporal timeReference) {
         super(targetDatum, domainOfValidity);
-        this.timeReference = timeReference.getTime();
+        this.timeReference = Objects.requireNonNull(timeReference);
+    }
+
+    /**
+     * Creates a new instance for the given target datum, domain of validity and time reference.
+     *
+     * @deprecated Replaced by {@link #TimeDependentBWP(GeodeticDatum, Extent, Temporal)}.
+     */
+    @Deprecated(since="1.5", forRemoval=true)
+    public TimeDependentBWP(final GeodeticDatum targetDatum, final Extent domainOfValidity, final Date timeReference) {
+        this(targetDatum, domainOfValidity, timeReference.toInstant());
     }
 
     /**
@@ -144,8 +160,8 @@
      *
      * @return the reference epoch for time-dependent parameters.
      */
-    public Date getTimeReference() {
-        return new Date(timeReference);
+    public Temporal getTimeReference() {
+        return timeReference;
     }
 
     /**
@@ -155,11 +171,11 @@
      * @return fractional number of tropical years since reference time, or {@code null}.
      */
     @Override
-    final DoubleDouble period(final Date time) {
+    final DoubleDouble period(final Temporal time) {
         if (time != null) {
-            final long millis = time.getTime() - timeReference;
-            if (millis != 0) {                                          // Returns null for 0 as an optimization.
-                return DoubleDouble.of(millis).divide(JULIAN_YEAR_LENGTH);
+            final Duration d = Duration.between(timeReference, time);
+            if (!d.isZero()) {      // Returns null for 0 as an optimization.
+                return DoubleDouble.of(d).divide(Constants.MILLIS_PER_TROPICAL_YEAR * Constants.NANOS_PER_MILLISECOND);
             }
         }
         return null;
@@ -285,6 +301,6 @@
      */
     @Override
     public int hashCode() {
-        return super.hashCode() ^ Long.hashCode(timeReference);
+        return super.hashCode() ^ timeReference.hashCode();
     }
 }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java
index 63527e9..dfd35e7 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java
@@ -57,7 +57,7 @@
 import org.apache.sis.util.logging.PerformanceLevel;
 import org.apache.sis.util.collection.Cache;
 import org.apache.sis.util.privy.CollectionsExt;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.Constants;
 import org.apache.sis.metadata.simple.SimpleCitation;
 import org.apache.sis.system.ReferenceQueueConsumer;
 import org.apache.sis.system.DelayedExecutor;
@@ -219,7 +219,7 @@
                 value = depth;
             } else {
                 text = "%s made available %d seconds ago";
-                value = Math.round((System.nanoTime() - timestamp) / (double) StandardDateFormat.NANOS_PER_SECOND);
+                value = Math.round((System.nanoTime() - timestamp) / (double) Constants.NANOS_PER_SECOND);
             }
             return String.format(text, Classes.getShortClassName(factory), value);
         }
@@ -493,7 +493,7 @@
                     caller = "create".concat(type.getSimpleName());
                 }
                 final Level level = PerformanceLevel.forDuration(time, TimeUnit.NANOSECONDS);
-                final Double duration = time / (double) StandardDateFormat.NANOS_PER_SECOND;
+                final Double duration = time / (double) Constants.NANOS_PER_SECOND;
                 final Messages resources = Messages.forLocale(null);
                 final LogRecord record;
                 if (code != null) {
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java
index 84c406a..f988b0e 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java
@@ -1703,6 +1703,8 @@
      * @param  cs     the coordinate system (never null).
      * @return the combined CRS, or {@code null} if the given information are not sufficient.
      * @throws FactoryException if an error occurred while creating the combined CRS.
+     *
+     * @todo Handle {@link DatumEnsemble}.
      */
     private static GeodeticCRS combine(final GeodeticDatum datum, final CoordinateSystem cs) throws FactoryException {
         final Map<String,?> properties = IdentifiedObjects.getProperties(datum, Datum.IDENTIFIERS_KEY);
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ReferenceKeeper.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ReferenceKeeper.java
index 63e06fc..25d466f 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ReferenceKeeper.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ReferenceKeeper.java
@@ -20,7 +20,7 @@
 import org.apache.sis.system.Configuration;
 import org.apache.sis.system.DelayedExecutor;
 import org.apache.sis.system.DelayedRunnable;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.Constants;
 
 
 /**
@@ -56,7 +56,7 @@
      * Time to wait before to remove entries from this map. Current value is 5 minutes.
      */
     @Configuration
-    private static final long EXPIRATION_TIME = 5L * 60 * StandardDateFormat.NANOS_PER_SECOND;
+    private static final long EXPIRATION_TIME = 5L * 60 * Constants.NANOS_PER_SECOND;
 
     /**
      * The objects to retain by strong reference. May contains duplicated values and {@code null} anywhere.
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
index 8cd976b..e64088b 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
@@ -41,10 +41,9 @@
 import java.sql.ResultSetMetaData;
 import java.sql.Statement;
 import java.sql.SQLException;
-import java.text.DateFormat;
-import java.text.ParseException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.time.temporal.Temporal;
 import javax.measure.Unit;
 import javax.measure.quantity.Angle;
 import javax.measure.quantity.Length;
@@ -118,11 +117,12 @@
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.Units;
 import org.apache.sis.pending.jdk.JDK16;
+import static org.apache.sis.util.privy.Constants.UTC;
 import static org.apache.sis.util.Utilities.equalsIgnoreMetadata;
-import static org.apache.sis.util.privy.StandardDateFormat.UTC;
 import static org.apache.sis.referencing.internal.ServicesForMetadata.CONNECTION;
 
 // Specific to the main branch:
+import org.apache.sis.util.privy.TemporalDate;
 import org.apache.sis.referencing.internal.ServicesForMetadata;
 import org.apache.sis.referencing.cs.DefaultParametricCS;
 import org.apache.sis.referencing.datum.DefaultParametricDatum;
@@ -244,12 +244,6 @@
     private Calendar calendar;
 
     /**
-     * The object to use for parsing dates, created when first needed. This is used for
-     * parsing the origin of temporal datum. This is an Apache SIS specific extension.
-     */
-    private DateFormat dateFormat;
-
-    /**
      * A pool of prepared statements. Keys are {@link String} objects related to their originating method
      * (for example "Ellipsoid" for {@link #createEllipsoid(String)}).
      */
@@ -1722,19 +1716,16 @@
                      * "date" type would have been better, but we do not modify the EPSG model.
                      */
                     case "temporal": {
-                        final Date originDate;
+                        final Temporal originDate;
                         if (Strings.isNullOrEmpty(anchor)) {
                             throw new FactoryDataException(resources().getString(Resources.Keys.DatumOriginShallBeDate));
                         }
-                        if (dateFormat == null) {
-                            dateFormat = new StandardDateFormat();      // Default to UTC timezone.
-                        }
                         try {
-                            originDate = dateFormat.parse(anchor);
-                        } catch (ParseException e) {
+                            originDate = StandardDateFormat.parseBest(anchor);
+                        } catch (RuntimeException e) {
                             throw new FactoryDataException(resources().getString(Resources.Keys.DatumOriginShallBeDate), e);
                         }
-                        datum = datumFactory.createTemporalDatum(properties, originDate);
+                        datum = datumFactory.createTemporalDatum(properties, TemporalDate.toDate(originDate));
                         break;
                     }
                     /*
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGInstaller.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGInstaller.java
index d789151..dfd4171 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGInstaller.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGInstaller.java
@@ -30,7 +30,7 @@
 import org.apache.sis.util.Exceptions;
 import org.apache.sis.metadata.sql.privy.ScriptRunner;
 import org.apache.sis.metadata.sql.privy.SQLUtilities;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.Constants;
 import org.apache.sis.system.Fallback;
 import org.apache.sis.util.resources.Messages;
 import org.apache.sis.util.logging.PerformanceLevel;
@@ -247,7 +247,7 @@
         time = System.nanoTime() - time;
         InstallationScriptProvider.log(Messages.forLocale(locale).getLogRecord(
                 PerformanceLevel.forDuration(time, TimeUnit.NANOSECONDS),
-                Messages.Keys.InsertDuration_2, numRows, time / (float) StandardDateFormat.NANOS_PER_SECOND));
+                Messages.Keys.InsertDuration_2, numRows, time / (float) Constants.NANOS_PER_SECOND));
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Epoch.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Epoch.java
index 80e0e3f..558fff3 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Epoch.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Epoch.java
@@ -27,7 +27,7 @@
 import org.apache.sis.io.wkt.FormattableObject;
 import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.referencing.privy.WKTKeywords;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.Constants;
 
 
 /**
@@ -70,7 +70,7 @@
                 fractionDigits = 2;
             } else if (epoch.isSupported(ChronoField.NANO_OF_DAY)) {
                 day = epoch.getLong(ChronoField.NANO_OF_DAY);
-                day /= StandardDateFormat.MILLISECONDS_PER_DAY * (long) StandardDateFormat.NANOS_PER_MILLISECOND;
+                day /= (double) Constants.NANOSECONDS_PER_DAY;
                 fractionDigits = (epoch.get(ChronoField.NANO_OF_SECOND) != 0) ? 16 : 8;
             }
             day += epoch.get(ChronoField.DAY_OF_YEAR) - 1;
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
index 35f9d4d..b9a1991 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
@@ -22,6 +22,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.ListIterator;
+import java.time.Duration;
 import javax.measure.Unit;
 import javax.measure.IncommensurableException;
 import javax.measure.quantity.Time;
@@ -36,6 +37,10 @@
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptorGroup;
+import org.apache.sis.parameter.TensorParameters;
+import org.apache.sis.measure.Units;
+import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.metadata.iso.extent.Extents;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.IdentifiedObjects;
@@ -46,6 +51,12 @@
 import org.apache.sis.referencing.privy.ReferencingUtilities;
 import org.apache.sis.referencing.internal.AnnotatedMatrix;
 import org.apache.sis.referencing.internal.Resources;
+import org.apache.sis.referencing.cs.CoordinateSystems;
+import org.apache.sis.referencing.datum.BursaWolfParameters;
+import org.apache.sis.referencing.datum.DefaultGeodeticDatum;
+import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
 import org.apache.sis.referencing.operation.provider.Affine;
 import org.apache.sis.referencing.operation.provider.DatumShiftMethod;
 import org.apache.sis.referencing.operation.provider.Geographic2Dto3D;
@@ -54,21 +65,14 @@
 import org.apache.sis.referencing.operation.provider.GeocentricToGeographic;
 import org.apache.sis.referencing.operation.provider.GeocentricAffine;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.privy.DoubleDouble;
 import org.apache.sis.util.privy.Constants;
-import org.apache.sis.measure.Units;
-import org.apache.sis.metadata.iso.citation.Citations;
-import org.apache.sis.metadata.iso.extent.Extents;
-import org.apache.sis.parameter.TensorParameters;
-import org.apache.sis.referencing.cs.CoordinateSystems;
-import org.apache.sis.referencing.datum.BursaWolfParameters;
-import org.apache.sis.referencing.datum.DefaultGeodeticDatum;
-import org.apache.sis.referencing.operation.matrix.Matrices;
-import org.apache.sis.referencing.operation.matrix.MatrixSIS;
-import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
+import org.apache.sis.util.privy.DoubleDouble;
 import org.apache.sis.util.resources.Vocabulary;
 import static org.apache.sis.util.Utilities.equalsIgnoreMetadata;
 
+// Specific to the main and geoapi-3.1 branches:
+import org.apache.sis.util.privy.TemporalDate;
+
 
 /**
  * Finds a conversion or transformation path from a source CRS to a target CRS.
@@ -859,15 +863,16 @@
         final TimeCS sourceCS = sourceCRS.getCoordinateSystem();
         final TimeCS targetCS = targetCRS.getCoordinateSystem();
         /*
-         * Compute the epoch shift.  The epoch is the time "0" in a particular coordinate reference system.
-         * For example, the epoch for java.util.Date object is january 1, 1970 at 00:00 UTC. We compute how
-         * much to add to a time in `sourceCRS` in order to get a time in `targetCRS`.
+         * Compute the epoch shift. The epoch is the "time zero" in a particular coordinate reference system.
+         * For example, the epoch of Java temporal objects (e.g. `Instant`) is january 1, 1970 at 00:00 UTC.
+         * We compute how much to add to a time in `sourceCRS` in order to get a time in `targetCRS`.
          * This "epoch shift" is in units of `targetCRS`.
          */
         final Unit<Time> targetUnit = targetCS.getAxis(0).getUnit().asType(Time.class);
-        DoubleDouble epochShift = DoubleDouble.of(sourceDatum.getOrigin().getTime());
-        epochShift = epochShift.subtract(targetDatum.getOrigin().getTime());
-        epochShift = DoubleDouble.of(Units.MILLISECOND.getConverterTo(targetUnit).convert(epochShift), true);
+        DoubleDouble epochShift = DoubleDouble.of(Duration.between(
+                TemporalDate.toTemporal(targetDatum.getOrigin()),
+                TemporalDate.toTemporal(sourceDatum.getOrigin())));
+        epochShift = DoubleDouble.of(Units.NANOSECOND.getConverterTo(targetUnit).convert(epochShift), true);
         /*
          * Check axis directions. The method `swapAndScaleAxes` should returns a matrix of size 2×2.
          * The element at index (0,0) may be +1 if source and target axes are in the same direction,
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ExtentSelector.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ExtentSelector.java
index 4b3f423..e5b4e05 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ExtentSelector.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ExtentSelector.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.referencing.privy;
 
-import java.util.Date;
 import java.time.Instant;
 import java.time.Duration;
 import org.opengis.metadata.extent.Extent;
@@ -25,6 +24,7 @@
 import org.apache.sis.math.MathFunctions;
 import org.apache.sis.measure.Range;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.pending.jdk.JDK23;
 
 
 /**
@@ -221,12 +221,10 @@
     public final boolean setExtentOfInterest(final Extent domain, final GeographicBoundingBox aoi, final Instant[] toi) {
         areaOfInterest = Extents.intersection(aoi, Extents.getGeographicBoundingBox(domain));
         minTOI = maxTOI = null;
-        final Range<Date> tr = Extents.getTimeRange(domain);
-        if (tr != null) {
-            Date t;
-            if ((t = tr.getMinValue()) != null) minTOI = t.toInstant();
-            if ((t = tr.getMaxValue()) != null) maxTOI = t.toInstant();
-        }
+        Extents.getTimeRange(domain, null).ifPresent((tr) -> {
+            minTOI = tr.getMinValue();
+            maxTOI = tr.getMaxValue();
+        });
         if (toi != null && toi.length != 0) {
             Instant t = toi[0];
             if (minTOI == null || (t != null && t.isAfter(minTOI))) {
@@ -367,7 +365,7 @@
      */
     private Duration overtime(final Instant startTime, final Instant endTime, final Duration intersection) {
         return (startTime != null && endTime != null && intersection != null)
-                ? round(Duration.between(startTime, endTime).minus(intersection)) : null;
+                ? round(JDK23.until(startTime, endTime).minus(intersection)) : null;
     }
 
     /**
@@ -379,11 +377,10 @@
      * @param  object  a user object associated to the given extent.
      */
     public void evaluate(final Extent domain, final T object) {
-        Date t;
-        final Range<Date> tr = Extents.getTimeRange(domain);
+        final Range<Instant> tr = Extents.getTimeRange(domain, null).orElse(null);
         evaluate(Extents.getGeographicBoundingBox(domain),
-                 (tr != null && (t = tr.getMinValue()) != null) ? t.toInstant() : null,
-                 (tr != null && (t = tr.getMaxValue()) != null) ? t.toInstant() : null,
+                 (tr != null) ? tr.getMinValue() : null,
+                 (tr != null) ? tr.getMaxValue() : null,
                  object);
     }
 
@@ -409,7 +406,7 @@
         if (tmax != null && maxTOI != null && tmax.isAfter (maxTOI)) tmax = maxTOI;
         final Duration duration;
         if (tmin != null && tmax != null) {
-            duration = Duration.between(tmin, tmax);
+            duration = JDK23.until(tmin, tmax);
             if (duration.isNegative()) return;
         } else {
             duration = null;
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/Formulas.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/Formulas.java
index 357eb6a..86350f4 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/Formulas.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/Formulas.java
@@ -82,15 +82,6 @@
     public static final double LONGITUDE_MAX = Numerics.MAX_INTEGER_CONVERTIBLE_TO_DOUBLE/2 * ANGULAR_TOLERANCE;
 
     /**
-     * The length of a <i>Julian year</i> in milliseconds.
-     * From Wikipedia, <q>In astronomy, a Julian year (symbol: <b>a</b>) is a unit of measurement of time
-     * defined as exactly 365.25 days of 86,400 SI seconds each.</q>.
-     *
-     * @see <a href="https://en.wikipedia.org/wiki/Julian_year_%28astronomy%29">Wikipedia: Julian year (astronomy)</a>
-     */
-    public static final long JULIAN_YEAR_LENGTH = 31557600000L;
-
-    /**
      * Maximum number of iterations for iterative computations. Defined in this {@code Formulas} class as a default value,
      * but some classes may use a derived value (for example twice this amount). This constant is mostly useful for identifying
      * places where iterations occur.
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/GeodeticObjectBuilder.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/GeodeticObjectBuilder.java
index 020b7df..42c5ae5 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/GeodeticObjectBuilder.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/GeodeticObjectBuilder.java
@@ -17,10 +17,10 @@
 package org.apache.sis.referencing.privy;
 
 import java.util.Map;
-import java.util.Date;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.function.BiConsumer;
+import java.time.temporal.Temporal;
 import javax.measure.Unit;
 import javax.measure.quantity.Time;
 import javax.measure.quantity.Length;
@@ -61,6 +61,9 @@
 import org.apache.sis.referencing.internal.Resources;
 import org.apache.sis.parameter.Parameters;
 
+// Specific to the main branch:
+import org.apache.sis.util.privy.TemporalDate;
+
 
 /**
  * Helper methods for building Coordinate Reference Systems and related objects.
@@ -533,12 +536,12 @@
      * Creates a temporal CRS from the given origin and temporal unit. For this method, the CRS name is optional:
      * if no {@code addName(…)} method has been invoked, then a default name will be used.
      *
-     * @param  origin  the epoch in milliseconds since January 1st, 1970 at midnight UTC.
+     * @param  origin  the origin of the temporal datum.
      * @param  unit    the unit of measurement.
      * @return a temporal CRS using the given origin and units.
      * @throws FactoryException if an error occurred while building the temporal CRS.
      */
-    public TemporalCRS createTemporalCRS(final Date origin, final Unit<Time> unit) throws FactoryException {
+    public TemporalCRS createTemporalCRS(final Temporal origin, final Unit<Time> unit) throws FactoryException {
         /*
          * Try to use one of the predefined datum and coordinate system if possible.
          * This not only saves a little bit of memory, but also provides better names.
@@ -582,7 +585,7 @@
             if (datum == null) {
                 final Object remarks    = properties.remove(TemporalCRS.REMARKS_KEY);
                 final Object identifier = properties.remove(TemporalCRS.IDENTIFIERS_KEY);
-                datum = factories.getDatumFactory().createTemporalDatum(properties, origin);
+                datum = factories.getDatumFactory().createTemporalDatum(properties, TemporalDate.toDate(origin));
                 properties.put(TemporalCRS.IDENTIFIERS_KEY, identifier);
                 properties.put(TemporalCRS.REMARKS_KEY,     remarks);
                 properties.put(TemporalCRS.NAME_KEY, datum.getName());      // Share the Identifier instance.
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/TemporalAccessor.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/TemporalAccessor.java
index 75bf192..02cd572 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/TemporalAccessor.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/TemporalAccessor.java
@@ -132,7 +132,7 @@
      * @param  target    the target temporal extent.
      */
     public void setTemporalExtent(final Envelope envelope, final DefaultTemporalExtent target) {
-        target.setBounds(timeCRS.toDate(envelope.getMinimum(dimension)),
-                         timeCRS.toDate(envelope.getMaximum(dimension)));
+        target.setBounds(timeCRS.toInstant(envelope.getMinimum(dimension)),
+                         timeCRS.toInstant(envelope.getMaximum(dimension)));
     }
 }
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/ElementTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/ElementTest.java
index 93c7d8a..409e88c 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/ElementTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/ElementTest.java
@@ -19,6 +19,7 @@
 import java.util.Map;
 import java.util.HashMap;
 import java.util.Locale;
+import java.time.Instant;
 import java.text.ParsePosition;
 import java.text.ParseException;
 import org.apache.sis.util.CharSequences;
@@ -26,7 +27,6 @@
 // Test dependencies
 import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.*;
-import org.apache.sis.test.TestUtilities;
 import org.apache.sis.test.TestCase;
 
 
@@ -182,7 +182,7 @@
     public void testPullDate() throws ParseException {
         Element element = parse("TimeOrigin[1858-11-17T00:00:00.0Z]");
         assertEquals("TimeOrigin", element.keyword);
-        assertEquals(TestUtilities.date("1858-11-17 00:00:00"), element.pullDate("date"));
+        assertEquals(Instant.parse("1858-11-17T00:00:00Z"), element.pullDate("date"));
         element.close(null);
     }
 
@@ -222,7 +222,7 @@
         assertEquals("Modified Julian", element.pullString("name"));
         Element inner = element.pullElement(AbstractParser.MANDATORY, "TimeOrigin");
         assertEquals("TimeOrigin", inner.keyword);
-        assertEquals(TestUtilities.date("1858-11-17 00:00:00"), inner.pullDate("date"));
+        assertEquals(Instant.parse("1858-11-17T00:00:00Z"), inner.pullDate("date"));
         inner.close(null);
         element.close(null);
     }
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
index a5d62e5..5d11c50 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
@@ -17,9 +17,9 @@
 package org.apache.sis.io.wkt;
 
 import java.util.Map;
-import java.util.Date;
 import java.util.Iterator;
 import java.util.Locale;
+import java.time.Instant;
 import java.text.ParsePosition;
 import java.text.ParseException;
 import javax.measure.Unit;
@@ -43,7 +43,10 @@
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.measure.Units;
-import static org.apache.sis.util.privy.StandardDateFormat.MILLISECONDS_PER_DAY;
+import static org.apache.sis.util.privy.Constants.SECONDS_PER_DAY;
+
+// Specific to the main and geoapi-3.1 branches:
+import org.apache.sis.util.privy.TemporalDate;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
@@ -1065,7 +1068,7 @@
         final TemporalDatum timeDatum = timeCRS.getDatum();
         assertNameAndIdentifierEqual("Time", 0, timeCRS);
         assertNameAndIdentifierEqual("Modified Julian", 0, timeDatum);
-        assertEquals(new Date(-40587L * MILLISECONDS_PER_DAY), timeDatum.getOrigin(), "epoch");
+        assertEquals(Instant.ofEpochSecond(-40587L * SECONDS_PER_DAY), TemporalDate.toTemporal(timeDatum.getOrigin()), "epoch");
 
         // No more CRS.
         assertFalse(components.hasNext());
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CommonCRSTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CommonCRSTest.java
index 14faed4..a4d9837 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CommonCRSTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CommonCRSTest.java
@@ -19,7 +19,10 @@
 import java.util.Date;
 import java.util.Map;
 import java.util.HashMap;
+import java.util.Locale;
+import java.util.TimeZone;
 import java.time.Instant;
+import java.text.SimpleDateFormat;
 import org.opengis.util.FactoryException;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.referencing.IdentifiedObject;
@@ -46,9 +49,11 @@
 import static org.apache.sis.test.Assertions.assertEqualsIgnoreMetadata;
 import static org.apache.sis.test.Assertions.assertMessageContains;
 import static org.apache.sis.test.TestUtilities.*;
+import static org.apache.sis.util.privy.Constants.UTC;
 
 // Specific to the main branch:
 import org.opengis.referencing.datum.VerticalDatumType;
+import org.apache.sis.util.privy.TemporalDate;
 import static org.apache.sis.test.GeoapiAssert.assertAxisDirectionsEqual;
 
 
@@ -59,11 +64,6 @@
  */
 public final class CommonCRSTest extends TestCase {
     /**
-     * Length of a day in milliseconds.
-     */
-    private static final double DAY_LENGTH = 24 * 60 * 60 * 1000;
-
-    /**
      * Creates a new test case.
      */
     public CommonCRSTest() {
@@ -252,8 +252,20 @@
      */
     @Test
     public void testTemporal() {
-        final double julianEpoch = CommonCRS.Temporal.JULIAN.datum().getOrigin().getTime() / DAY_LENGTH;
-        assertTrue(julianEpoch < 0);
+        final var julianEpoch = TemporalDate.toInstant(CommonCRS.Temporal.JULIAN.datum().getOrigin());
+        final double SECONDS_PER_DAY = Constants.SECONDS_PER_DAY;
+        final double julianEpochSecond = julianEpoch.getEpochSecond() / SECONDS_PER_DAY;
+        assertTrue(julianEpochSecond < 0);
+        /*
+         * We need to use `java.text.DateFormat` rather than `Instant.parse(String)` because
+         * they have different policy regarding the calendar for dates before October 15, 1582.
+         * The `java.time` classes use the proleptic Gregorian calendar while `java.text` uses
+         * the prolectic Julian calendar. The latter is what we need for this test.
+         */
+        final var dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CANADA);
+        dateFormat.setTimeZone(TimeZone.getTimeZone(UTC));
+        dateFormat.setLenient(false);
+
         for (final CommonCRS.Temporal e : CommonCRS.Temporal.values()) {
             final String epoch;
             final double days;
@@ -274,8 +286,8 @@
             Validators.validate(crs);
             assertSame(datum, e.datum(), name);             // Datum before CRS creation.
             assertSame(crs.getDatum(), e.datum(), name);    // Datum after CRS creation.
-            assertEquals(epoch, format(origin), name);
-            assertEquals(days, origin.getTime() / DAY_LENGTH - julianEpoch, name);
+            assertEquals(epoch, dateFormat.format(origin), name);
+            assertEquals(days, origin.getTime() / (1000*SECONDS_PER_DAY) - julianEpochSecond, name);
             switch (e) {
                 case JAVA: {
                     assertNameContains(datum, "Unix/POSIX");
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultTemporalCRSTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultTemporalCRSTest.java
index edfcb92..2ee0116 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultTemporalCRSTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultTemporalCRSTest.java
@@ -22,8 +22,8 @@
 import java.util.Map;
 import org.apache.sis.referencing.datum.DefaultTemporalDatum;
 import org.apache.sis.io.wkt.Convention;
-import static org.apache.sis.util.privy.StandardDateFormat.MILLISECONDS_PER_DAY;
-import static org.apache.sis.util.privy.StandardDateFormat.NANOS_PER_MILLISECOND;
+import static org.apache.sis.util.privy.Constants.MILLISECONDS_PER_DAY;
+import static org.apache.sis.util.privy.Constants.NANOS_PER_MILLISECOND;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
@@ -109,10 +109,10 @@
      */
     @Test
     public void testDateConversionWithNanos() {
-        final DefaultTemporalDatum datum = new DefaultTemporalDatum(
+        final var datum = new DefaultTemporalDatum(
                 Map.of(DefaultTemporalDatum.NAME_KEY, "For test"),
-                new Date(10000L * MILLISECONDS_PER_DAY + 12345));                        // 1997-05-19T00:00:12.345Z
-        final DefaultTemporalCRS crs = new DefaultTemporalCRS(
+                Instant.ofEpochMilli(10000L * MILLISECONDS_PER_DAY + 12345));       // 1997-05-19T00:00:12.345Z
+        final var crs = new DefaultTemporalCRS(
                 Map.of(DefaultTemporalCRS.NAME_KEY, datum.getName()),
                 datum, HardCodedCS.DAYS);
         /*
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/BursaWolfParametersTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/BursaWolfParametersTest.java
index 9c85951..a97e6cd 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/BursaWolfParametersTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/BursaWolfParametersTest.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.referencing.datum;
 
-import java.util.Date;
 import org.opengis.referencing.operation.Matrix;
 import org.apache.sis.metadata.iso.extent.Extents;
 import org.apache.sis.metadata.iso.extent.DefaultExtent;
@@ -105,7 +104,7 @@
     }
 
     /**
-     * Invokes {@link BursaWolfParameters#getPositionVectorTransformation(Date)}
+     * Invokes {@link BursaWolfParameters#getPositionVectorTransformation(Temporal)}
      * and compares with our own matrix calculated using double arithmetic.
      */
     private static MatrixSIS getPositionVectorTransformation(final BursaWolfParameters p) {
@@ -150,7 +149,7 @@
     }
 
     /**
-     * Tests {@link BursaWolfParameters#getPositionVectorTransformation(Date)}.
+     * Tests {@link BursaWolfParameters#getPositionVectorTransformation(Temporal)}.
      * This test transform a point from WGS72 to WGS84, and conversely,
      * as documented in the example section of EPSG operation method 9606.
      *
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/DefaultTemporalDatumTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/DefaultTemporalDatumTest.java
index fbf6fee..0adab08 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/DefaultTemporalDatumTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/DefaultTemporalDatumTest.java
@@ -16,13 +16,13 @@
  */
 package org.apache.sis.referencing.datum;
 
-import java.util.Date;
 import java.util.HashMap;
+import java.time.ZoneOffset;
+import java.time.OffsetDateTime;
 import java.io.InputStream;
 import jakarta.xml.bind.JAXBException;
 import org.apache.sis.io.wkt.Convention;
 import org.apache.sis.referencing.ImmutableIdentifier;
-import static org.apache.sis.util.privy.StandardDateFormat.MILLISECONDS_PER_DAY;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
@@ -38,6 +38,9 @@
 import static org.opengis.referencing.ReferenceSystem.*;
 import static org.apache.sis.test.GeoapiAssert.assertIdentifierEquals;
 
+// Specific to the main and geoapi-3.1 branches:
+import org.apache.sis.util.privy.TemporalDate;
+
 
 /**
  * Tests the {@link DefaultTemporalDatum} class.
@@ -62,14 +65,16 @@
     }
 
     /**
-     * November 17, 1858 at 00:00 UTC as a Java timestamp.
+     * November 17, 1858 at 00:00 UTC.
      */
-    private static final long ORIGIN = -40587L * MILLISECONDS_PER_DAY;
+    private static final OffsetDateTime ORIGIN = OffsetDateTime.of(1858, 11, 17, 0, 0, 0, 0, ZoneOffset.UTC);
 
     /**
      * Creates the temporal datum to use for testing purpose.
+     *
+     * @param local  whether the datum origin should be a local date.
      */
-    private static DefaultTemporalDatum create() {
+    private static DefaultTemporalDatum create(final boolean local) {
         final var properties = new HashMap<String,Object>(4);
         assertNull(properties.put(IDENTIFIERS_KEY,
                 new ImmutableIdentifier(HardCodedCitations.SIS, "SIS", "MJ")));
@@ -77,7 +82,7 @@
         assertNull(properties.put(SCOPE_KEY, "History."));
         assertNull(properties.put(REMARKS_KEY,
                 "Time measured as days since November 17, 1858 at 00:00 UTC."));
-        return new DefaultTemporalDatum(properties, new Date(ORIGIN));
+        return new DefaultTemporalDatum(properties, local ? ORIGIN.toLocalDate() : ORIGIN);
     }
 
     /**
@@ -85,7 +90,7 @@
      */
     @Test
     public void testConsistency() {
-        assertEquals(HardCodedDatum.MODIFIED_JULIAN.getOrigin(), new Date(ORIGIN));
+        assertEquals(TemporalDate.toTemporal(HardCodedDatum.MODIFIED_JULIAN.getOrigin()), ORIGIN.toInstant());
     }
 
     /**
@@ -98,7 +103,7 @@
      */
     @Test
     public void testToWKT() {
-        final DefaultTemporalDatum datum = create();
+        final DefaultTemporalDatum datum = create(true);
         assertWktEquals(Convention.WKT1, "TDATUM[“Modified Julian”, TIMEORIGIN[1858-11-17], AUTHORITY[“SIS”, “MJ”]]", datum);
         assertWktEquals(Convention.WKT2, "TDATUM[“Modified Julian”, TIMEORIGIN[1858-11-17], ID[“SIS”, “MJ”]]", datum);
         assertWktEquals(Convention.WKT2_SIMPLIFIED, "TimeDatum[“Modified Julian”, TimeOrigin[1858-11-17], Id[“SIS”, “MJ”]]", datum);
@@ -111,7 +116,7 @@
      */
     @Test
     public void testMarshalling() throws JAXBException {
-        final DefaultTemporalDatum datum = create();
+        final DefaultTemporalDatum datum = create(false);
         assertMarshalEqualsFile(openTestFile(), datum, "xmlns:*", "xsi:schemaLocation");
     }
 
@@ -128,6 +133,6 @@
         assertEquals("Modified Julian", datum.getName().getCode());
         assertRemarksEquals("Time measured as days since November 17, 1858 at 00:00 UTC.", datum, null);
         assertEquals("History.", getScope(datum));
-        assertEquals(new Date(ORIGIN), datum.getOrigin());
+        assertEquals(ORIGIN, TemporalDate.toTemporal(datum.getOrigin()));
     }
 }
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/HardCodedDatum.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/HardCodedDatum.java
index 43903a2..e79d091 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/HardCodedDatum.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/HardCodedDatum.java
@@ -16,14 +16,14 @@
  */
 package org.apache.sis.referencing.datum;
 
-import java.util.Date;
 import java.util.Map;
 import java.util.HashMap;
+import java.time.Instant;
 import org.opengis.referencing.datum.PixelInCell;
 import org.apache.sis.referencing.NamedIdentifier;
 import org.apache.sis.referencing.internal.VerticalDatumTypes;
 import org.apache.sis.measure.Units;
-import static org.apache.sis.util.privy.StandardDateFormat.MILLISECONDS_PER_DAY;
+import static org.apache.sis.util.privy.Constants.SECONDS_PER_DAY;
 
 // Test dependencies
 import org.apache.sis.metadata.iso.citation.HardCodedCitations;
@@ -134,14 +134,14 @@
      */
     public static final DefaultTemporalDatum UNIX = new DefaultTemporalDatum(
             properties("UNIX", null, null),
-            new Date(0));
+            Instant.EPOCH);
 
     /**
      * Default datum for time measured since November 17, 1858 at 00:00 UTC.
      */
     public static final DefaultTemporalDatum MODIFIED_JULIAN = new DefaultTemporalDatum(
             properties("Modified Julian", null, null),
-            new Date(-40587L * MILLISECONDS_PER_DAY));
+            Instant.ofEpochSecond(-40587L * SECONDS_PER_DAY));
 
     /**
      * A parametric datum for day of year, without any particular year.
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/TimeDependentBWPTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/TimeDependentBWPTest.java
index 471c5fb..9aca865 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/TimeDependentBWPTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/TimeDependentBWPTest.java
@@ -16,18 +16,19 @@
  */
 package org.apache.sis.referencing.datum;
 
-import java.util.Date;
+import java.time.Instant;
+import java.time.temporal.Temporal;
+import java.time.temporal.ChronoField;
 import org.opengis.referencing.operation.Matrix;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.matrix.NoninvertibleMatrixException;
-import static org.apache.sis.referencing.privy.Formulas.JULIAN_YEAR_LENGTH;
+import static org.apache.sis.util.privy.Constants.MILLIS_PER_TROPICAL_YEAR;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.test.TestCase;
-import static org.apache.sis.test.TestUtilities.date;
 
 // Specific to the main branch:
 import static org.apache.sis.test.GeoapiAssert.assertMatrixEquals;
@@ -51,7 +52,7 @@
      * For the purpose of this test, the target datum does not matter anyway.
      */
     private static TimeDependentBWP create() {
-        final TimeDependentBWP p = new TimeDependentBWP(GeodeticDatumMock.WGS84, null, date("1994-01-01 00:00:00"));
+        final var p = new TimeDependentBWP(GeodeticDatumMock.WGS84, null, Instant.parse("1994-01-01T00:00:00Z"));
         p.tX = -0.08468;    p.dtX = +1.42;
         p.tY = -0.01942;    p.dtY = +1.34;
         p.tZ = +0.03201;    p.dtZ = +0.90;
@@ -101,9 +102,11 @@
          * geocentric coordinates on ITRF2008 at epoch 2013.9.
          */
         final TimeDependentBWP p = create();
-        final Date time = p.getTimeReference();
-        time.setTime(time.getTime() + StrictMath.round((2013.9 - 1994) * JULIAN_YEAR_LENGTH));
-        assertEquals(date("2013-11-25 11:24:00"), time);
+        Temporal time = p.getTimeReference();
+        long t = time.getLong(ChronoField.INSTANT_SECONDS);
+        t += StrictMath.round((2013.9 - 1994) * (MILLIS_PER_TROPICAL_YEAR / 1000.0));
+        time = Instant.ofEpochSecond(t);
+        assertEquals(Instant.parse("2013-11-25T07:40:16Z"), time);
         /*
          * Transform the point given in the EPSG example and compare with the coordinate
          * that we obtain if we do the calculation ourself using the intermediate values
@@ -124,7 +127,7 @@
     }
 
     /**
-     * Compares the coordinates calculated with the {@link TimeDependentBWP#getPositionVectorTransformation(Date)}
+     * Compares the coordinates calculated with the {@link TimeDependentBWP#getPositionVectorTransformation(Temporal)}
      * matrix with the coordinates calculated ourselves from the numbers given in the EPSG examples. Note that the
      * EPSG documentation truncates the numerical values given in their example, so it is normal that we have a
      * slight difference.
@@ -182,7 +185,7 @@
      * We find a difference of 13, 9 and 3 millimetres along the X, Y and Z axis respectively.
      *
      * <p>The purpose of this test is to ensure that we get the same errors when calculating from the corrected values
-     * provided by EPSG, rather than as an error in {@link TimeDependentBWP#getPositionVectorTransformation(Date)}</p>
+     * provided by EPSG, rather than as an error in {@link TimeDependentBWP#getPositionVectorTransformation(Temporal)}</p>
      */
     @Test
     public void testEpsgCalculation() {
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/ConcurrentAuthorityFactoryTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/ConcurrentAuthorityFactoryTest.java
index ffc67ff..e9bf014 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/ConcurrentAuthorityFactoryTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/ConcurrentAuthorityFactoryTest.java
@@ -24,7 +24,7 @@
 import java.util.function.Supplier;
 import java.lang.reflect.Field;
 import org.opengis.util.FactoryException;
-import static org.apache.sis.util.privy.StandardDateFormat.NANOS_PER_MILLISECOND;
+import static org.apache.sis.util.privy.Constants.NANOS_PER_MILLISECOND;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/ServicesForMetadataTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/ServicesForMetadataTest.java
index c64bf5b..d0c73dd 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/ServicesForMetadataTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/ServicesForMetadataTest.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.referencing.internal;
 
-import java.util.Date;
 import org.opengis.geometry.Envelope;
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.metadata.extent.VerticalExtent;
@@ -24,7 +23,6 @@
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
 import org.apache.sis.metadata.iso.extent.DefaultVerticalExtent;
-import org.apache.sis.metadata.iso.extent.DefaultTemporalExtent;
 import org.apache.sis.metadata.iso.extent.DefaultSpatialTemporalExtent;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.referencing.CommonCRS;
@@ -33,7 +31,6 @@
 import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.test.TestCase;
-import org.apache.sis.test.TestUtilities;
 import org.apache.sis.referencing.crs.HardCodedCRS;
 import static org.apache.sis.test.Assertions.assertEqualsIgnoreMetadata;
 import static org.apache.sis.test.TestUtilities.getSingleton;
@@ -66,7 +63,7 @@
      */
     @SuppressWarnings("fallthrough")
     private static GeneralEnvelope createEnvelope(final CoordinateReferenceSystem crs) {
-        final GeneralEnvelope envelope = new GeneralEnvelope(crs);
+        final var envelope = new GeneralEnvelope(crs);
         switch (crs.getCoordinateSystem().getDimension()) {
             default: throw new AssertionError();
             case 4: envelope.setRange(3, 51000, 52000);                 // Fall through
@@ -106,7 +103,7 @@
      */
     @Test
     public void testSetGeographicBoundsFrom3D() throws TransformException {
-        final DefaultGeographicBoundingBox box = new DefaultGeographicBoundingBox();
+        final var box = new DefaultGeographicBoundingBox();
         box.setBounds(createEnvelope(HardCodedCRS.WGS84_3D));
         verifySpatialExtent(box);
     }
@@ -119,7 +116,7 @@
      */
     @Test
     public void testSetGeographicBoundsFrom4D() throws TransformException {
-        final DefaultGeographicBoundingBox box = new DefaultGeographicBoundingBox();
+        final var box = new DefaultGeographicBoundingBox();
         box.setBounds(createEnvelope(HardCodedCRS.GEOID_4D));
         verifySpatialExtent(box);
     }
@@ -132,7 +129,7 @@
      */
     @Test
     public void testSetVerticalBoundsFromEllipsoid() throws TransformException {
-        final DefaultVerticalExtent extent = new DefaultVerticalExtent();
+        final var extent = new DefaultVerticalExtent();
         extent.setBounds(createEnvelope(HardCodedCRS.WGS84_3D));
         verifyVerticalExtent(CommonCRS.Vertical.ELLIPSOIDAL, extent);
     }
@@ -145,7 +142,7 @@
      */
     @Test
     public void testSetVerticalBoundsFromGeoid() throws TransformException {
-        final DefaultVerticalExtent extent = new DefaultVerticalExtent();
+        final var extent = new DefaultVerticalExtent();
         extent.setBounds(createEnvelope(HardCodedCRS.GEOID_4D));
         verifyVerticalExtent(CommonCRS.Vertical.MEAN_SEA_LEVEL, extent);
     }
@@ -157,7 +154,7 @@
      */
     @Test
     public void testSetSpatialTemporalBounds() throws TransformException {
-        final DefaultSpatialTemporalExtent extent = new DefaultSpatialTemporalExtent();
+        final var extent = new DefaultSpatialTemporalExtent();
         extent.setBounds(createEnvelope(HardCodedCRS.GEOID_3D));
         verifySpatialExtent((GeographicBoundingBox) getSingleton(extent.getSpatialExtent()));
         verifyVerticalExtent(CommonCRS.Vertical.MEAN_SEA_LEVEL, extent.getVerticalExtent());
@@ -171,7 +168,7 @@
      */
     @Test
     public void testSetGeographicBoundsCrossingAntimeridian() throws TransformException {
-        final DefaultGeographicBoundingBox box = new DefaultGeographicBoundingBox();
+        final var box = new DefaultGeographicBoundingBox();
         final GeneralEnvelope envelope = createEnvelope(HardCodedCRS.WGS84);
         envelope.setRange(0, 170, 195);
         box.setBounds(envelope);
@@ -193,29 +190,9 @@
      */
     @Test
     public void testVerticalIntersection() throws TransformException {
-        final DefaultVerticalExtent e1 = new DefaultVerticalExtent(1000, 2000, HardCodedCRS.ELLIPSOIDAL_HEIGHT_cm);
-        final DefaultVerticalExtent e2 = new DefaultVerticalExtent(15,   25,   HardCodedCRS.ELLIPSOIDAL_HEIGHT);
+        final var e1 = new DefaultVerticalExtent(1000, 2000, HardCodedCRS.ELLIPSOIDAL_HEIGHT_cm);
+        final var e2 = new DefaultVerticalExtent(15,   25,   HardCodedCRS.ELLIPSOIDAL_HEIGHT);
         e1.intersect(e2);
         assertEquals(new DefaultVerticalExtent(1500, 2000, HardCodedCRS.ELLIPSOIDAL_HEIGHT_cm), e1);
     }
-
-    /**
-     * Tests {@link DefaultTemporalExtent#intersect(TemporalExtent)}.
-     *
-     * @throws TransformException if the transformation failed.
-     */
-    @Test
-    public void testTemporalIntersection() throws TransformException {
-        final DefaultTemporalExtent e1 = new DefaultTemporalExtent();
-        final DefaultTemporalExtent e2 = new DefaultTemporalExtent();
-        final Date t1 = TestUtilities.date("2016-12-05 19:45:20");
-        final Date t2 = TestUtilities.date("2017-02-18 02:12:50");
-        final Date t3 = TestUtilities.date("2017-11-30 23:50:00");
-        final Date t4 = TestUtilities.date("2018-05-20 12:30:45");
-        e1.setBounds(t1, t3);
-        e2.setBounds(t2, t4);
-        e1.intersect(e2);
-        assertEquals(t2, e1.getStartTime(), "startTime");
-        assertEquals(t3, e1.getEndTime(), "endTime");
-    }
 }
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
index fa221a1..d1c91e7 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
@@ -52,6 +52,7 @@
 import org.apache.sis.referencing.crs.DefaultDerivedCRS;
 import org.apache.sis.io.wkt.WKTFormat;
 import org.apache.sis.measure.Units;
+import static org.apache.sis.util.privy.Constants.SECONDS_PER_DAY;
 import static org.apache.sis.referencing.privy.Formulas.LINEAR_TOLERANCE;
 import static org.apache.sis.referencing.privy.Formulas.ANGULAR_TOLERANCE;
 import static org.apache.sis.referencing.privy.PositionalAccuracyConstant.DATUM_SHIFT_APPLIED;
@@ -951,7 +952,7 @@
                     1, 0, 0, 0,
                     0, 1, 0, 0,
                     0, 0, 0, 0,
-                    0, 0, 1./(24*60*60), 40587,
+                    0, 0, 1./SECONDS_PER_DAY, 40587,
                     0, 0, 0, 1
                 }), linear.getMatrix(), 1E-12, "transform.matrix");
 
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/projection/MercatorMethodComparison.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/projection/MercatorMethodComparison.java
index 30f40d4..d83bdc2 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/projection/MercatorMethodComparison.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/projection/MercatorMethodComparison.java
@@ -26,7 +26,7 @@
 import org.apache.sis.math.StatisticsFormat;
 import org.apache.sis.referencing.internal.Resources;
 import org.apache.sis.metadata.privy.ReferencingServices;
-import static org.apache.sis.util.privy.StandardDateFormat.NANOS_PER_SECOND;
+import static org.apache.sis.util.privy.Constants.NANOS_PER_SECOND;
 
 // Test dependencies
 import org.apache.sis.test.Benchmark;
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/projection/ProjectionBenchmark.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/projection/ProjectionBenchmark.java
index cd093e9..673af07 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/projection/ProjectionBenchmark.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/projection/ProjectionBenchmark.java
@@ -28,7 +28,6 @@
 import org.apache.sis.measure.Latitude;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.util.privy.Constants;
-import org.apache.sis.util.privy.StandardDateFormat;
 import org.apache.sis.referencing.operation.provider.AbstractProvider;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
@@ -219,7 +218,7 @@
             long time = System.nanoTime();
             projection.transform(sources, 0, targets, 0, NUM_POINTS);
             time = System.nanoTime() - time;
-            final double seconds = time / (double) StandardDateFormat.NANOS_PER_SECOND;
+            final double seconds = time / (double) Constants.NANOS_PER_SECOND;
             System.out.printf("%s time: %1.4f%n", performance.name(), seconds);
             performance.accept(seconds);
         }
@@ -235,7 +234,7 @@
             kernel.transform(targets, 0, targets, 0, NUM_POINTS);
             time = System.nanoTime() - time;
             denormalize.transform(targets, 0, targets, 0, NUM_POINTS);
-            final double seconds = time / (double) StandardDateFormat.NANOS_PER_SECOND;
+            final double seconds = time / (double) Constants.NANOS_PER_SECOND;
             System.out.printf("%s time: %1.4f%n", performance.name(), seconds);
             performance.accept(seconds);
         }
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/privy/ExtentSelectorTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/privy/ExtentSelectorTest.java
index d8a05fa..41758cb 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/privy/ExtentSelectorTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/privy/ExtentSelectorTest.java
@@ -16,7 +16,7 @@
  */
 package org.apache.sis.referencing.privy;
 
-import java.util.Date;
+import java.time.Instant;
 import java.time.Duration;
 import org.opengis.metadata.extent.Extent;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
@@ -150,11 +150,11 @@
      *                   {@code true} for associating a larger geographic area.
      */
     private static Extent time(final long startTime, final long endTime, final boolean largeArea) {
-        final DefaultGeographicBoundingBox bbox = new DefaultGeographicBoundingBox(
+        final var bbox = new DefaultGeographicBoundingBox(
                 largeArea ? -20 : -10, 10,
                 largeArea ?  10 :  20, 30);
-        final DefaultTemporalExtent range = new DefaultTemporalExtent();
-        range.setBounds(new Date(startTime), new Date(endTime));
+        final var range = new DefaultTemporalExtent();
+        range.setBounds(Instant.ofEpochMilli(startTime), Instant.ofEpochMilli(endTime));
         return new DefaultExtent(null, bbox, null, range);
     }
 
@@ -162,7 +162,7 @@
      * Creates the selector to use for testing purpose.
      */
     private ExtentSelector<Integer> create(final Extent aoi) {
-        ExtentSelector<Integer> selector = new ExtentSelector<>(aoi);
+        final var selector = new ExtentSelector<Integer>(aoi);
         selector.alternateOrdering = alternateOrdering;
         selector.setTimeGranularity(granularity);
         return selector;
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/privy/FormulasTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/privy/FormulasTest.java
index 55ed090..6bb6d68 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/privy/FormulasTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/privy/FormulasTest.java
@@ -52,14 +52,6 @@
     }
 
     /**
-     * Verifies the {@link Formulas#JULIAN_YEAR_LENGTH} constant.
-     */
-    @Test
-    public void verifyJulianYearLength() {
-        assertEquals(StrictMath.round(365.25 * 24 * 60 * 60 * 1000), Formulas.JULIAN_YEAR_LENGTH);
-    }
-
-    /**
      * Tests {@link Formulas#isPoleToPole(double, double)}.
      */
     @Test
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/test/integration/MetadataTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/test/integration/MetadataTest.java
index b66a6e2..06fcd20 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/test/integration/MetadataTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/test/integration/MetadataTest.java
@@ -23,6 +23,7 @@
 import java.util.Set;
 import java.util.Map;
 import java.util.HashMap;
+import java.time.Instant;
 import java.io.StringWriter;
 import java.nio.charset.StandardCharsets;
 import jakarta.xml.bind.Marshaller;
@@ -244,7 +245,8 @@
                         nameAndIdentifier("D28", "Depth below D28", "CRS for testing purpose"), datum, cs);
 
                 final var temporal = new DefaultTemporalExtent();
-                temporal.setBounds(TestUtilities.date("1990-06-05 00:00:00"), TestUtilities.date("1990-07-02 00:00:00"));
+                temporal.setBounds(Instant.parse("1990-06-05T00:00:00Z"),
+                                   Instant.parse("1990-07-02T00:00:00Z"));
                 identification.setExtents(Set.of(new DefaultExtent(
                         null,
                         new DefaultGeographicBoundingBox(1.1666, 1.1667, 36.4, 36.6),
diff --git a/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/MetadataReader.java b/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/MetadataReader.java
index c5a5199..81d0d7f 100644
--- a/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/MetadataReader.java
+++ b/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/MetadataReader.java
@@ -23,7 +23,6 @@
 import java.nio.charset.StandardCharsets;
 import java.util.Optional;
 import java.util.Arrays;
-import java.util.Date;
 import java.util.Locale;
 import java.util.EnumMap;
 import java.util.regex.Matcher;
@@ -64,7 +63,6 @@
 import org.apache.sis.referencing.operation.provider.PolarStereographicB;
 import org.apache.sis.referencing.operation.provider.TransverseMercator;
 import org.apache.sis.storage.base.MetadataBuilder;
-import org.apache.sis.util.privy.StandardDateFormat;
 import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.Strings;
 import static org.apache.sis.util.privy.CollectionsExt.singletonOrNull;
@@ -429,8 +427,7 @@
              * Example: "2014-03-12T06:06:35Z".
              */
             case "FILE_DATE": {
-                addCitationDate(StandardDateFormat.toDate(OffsetDateTime.parse(value)),
-                                DateType.CREATION, MetadataBuilder.Scope.ALL);
+                addCitationDate(OffsetDateTime.parse(value), DateType.CREATION, MetadataBuilder.Scope.ALL);
                 break;
             }
             /*
@@ -809,9 +806,8 @@
         final Temporal st = sceneTime;
         if (st != null) {
             sceneTime = null;                   // Clear now in case an exception it thrown below.
-            final Date t = StandardDateFormat.toDate(st);
-            addAcquisitionTime(t);
-            addTemporalExtent(t, t);
+            addAcquisitionTime(st);
+            addTemporalExtent(st, st);
         }
     }
 
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index 960d1b7..723f165 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -1011,7 +1011,7 @@
              */
             case TAG_DATE_TIME: {
                 for (final String value : type.readAsStrings(input(), count, encoding())) {
-                    metadata.addCitationDate(reader.store.getDateFormat().parse(value),
+                    metadata.addCitationDate(reader.store.getDateFormat().parse(value).toInstant(),
                             DateType.CREATION, ImageMetadataBuilder.Scope.RESOURCE);
                 }
                 break;
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java
index 24a489a..2f2a65f 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java
@@ -1141,6 +1141,7 @@
                 if (!Units.METRE.equals(linearUnit)) {
                     cs = replaceLinearUnit(cs, linearUnit);
                 }
+                // TODO: datum should be DatumEnsemble for some case such as EPSG:4326.
                 final GeodeticCRS crs = getCRSFactory().createGeocentricCRS(properties(getOrDefault(names, GCRS)), datum, cs);
                 lastName = crs.getName();
                 return crs;
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/MetadataReader.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/MetadataReader.java
index 73ac736..868fff0 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/MetadataReader.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/MetadataReader.java
@@ -18,7 +18,6 @@
 
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.Date;
 import java.util.List;
 import java.util.Set;
 import java.util.Map;
@@ -28,6 +27,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.io.IOException;
+import java.time.temporal.Temporal;
 import ucar.nc2.constants.CF;       // String constants are copied by the compiler with no UCAR reference left.
 import ucar.nc2.constants.CDM;      // idem
 import ucar.nc2.constants.ACDD;     // idem
@@ -757,18 +757,18 @@
             hasExtent = true;
         }
         /*
-         * Get the start and end times as Date objects if available, or as numeric values otherwise.
-         * In the latter case, the unit symbol tells how to convert to Date objects.
+         * Get the start and end times as temporal objects if available, or as numeric values otherwise.
+         * In the latter case, the unit symbol tells how to convert to temporal objects.
          */
-        Date startTime = decoder.dateValue(TIME.MINIMUM);
-        Date endTime   = decoder.dateValue(TIME.MAXIMUM);
+        Temporal startTime = decoder.dateValue(TIME.MINIMUM);
+        Temporal endTime   = decoder.dateValue(TIME.MAXIMUM);
         if (startTime == null && endTime == null) {
             final Number tmin = decoder.numericValue(TIME.MINIMUM);
             final Number tmax = decoder.numericValue(TIME.MAXIMUM);
             if (tmin != null || tmax != null) {
                 final String symbol = stringValue(TIME.UNITS);
                 if (symbol != null) {
-                    final Date[] dates = decoder.numberToDate(symbol, tmin, tmax);
+                    final Temporal[] dates = decoder.numberToDate(symbol, tmin, tmax);
                     startTime = dates[0];
                     endTime   = dates[1];
                 }
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/AxisType.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/AxisType.java
index 604b907..a66b1c9 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/AxisType.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/AxisType.java
@@ -18,7 +18,6 @@
 
 import java.util.Map;
 import java.util.HashMap;
-import java.util.Locale;
 import ucar.nc2.constants.CF;       // String constants are copied by the compiler with no UCAR reference left.
 import javax.measure.Unit;
 import org.opengis.referencing.cs.AxisDirection;
@@ -100,7 +99,7 @@
      * @return axis abbreviation for the given type or name, or {@code null} if none.
      */
     private static Character abbreviation(final String type) {
-        return (type != null) ? TYPES.get(type.toLowerCase(Locale.US)) : null;
+        return (type != null) ? TYPES.get(type.toLowerCase(Decoder.DATA_LOCALE)) : null;
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java
index 843266b..1e280ae 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java
@@ -18,7 +18,6 @@
 
 import java.util.Map;
 import java.util.List;
-import java.util.Date;
 import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.StringJoiner;
@@ -61,6 +60,7 @@
 import org.apache.sis.measure.Units;
 
 // Specific to the main branch:
+import org.apache.sis.util.privy.TemporalDate;
 import org.apache.sis.referencing.factory.GeodeticObjectFactory;
 
 
@@ -136,6 +136,7 @@
 
     /**
      * The datum created by {@link #createDatum(DatumFactory, Map)}.
+     * At least one of {@code datum} and {@link #datumEnsemble} shall be initialized.
      */
     protected D datum;
 
@@ -968,7 +969,7 @@
                     datum = c.datum();
                 } else {
                     properties = properties("Time since " + epoch);
-                    datum = factory.createTemporalDatum(properties, Date.from(epoch));
+                    datum = factory.createTemporalDatum(properties, TemporalDate.toDate(epoch));
                 }
             }
         }
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java
index 1a25de6..0791290 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java
@@ -21,7 +21,6 @@
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
-import java.util.Locale;
 import java.util.ServiceLoader;
 import java.util.function.Function;
 import java.awt.Color;
@@ -482,7 +481,7 @@
         final Map<String,Object> definition = new HashMap<>();
         definition.put(CF.GRID_MAPPING_NAME, method);
         for (final String name : node.getAttributeNames()) try {
-            final String ln = name.toLowerCase(Locale.US);
+            final String ln = name.toLowerCase(Decoder.DATA_LOCALE);
             Object value;
             switch (ln) {
                 case CF.GRID_MAPPING_NAME: continue;        // Already stored.
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
index abee3b6..a5cfe8b 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
@@ -22,12 +22,14 @@
 import java.util.HashMap;
 import java.util.Collection;
 import java.util.Objects;
-import java.util.Date;
-import java.util.TimeZone;
+import java.util.Locale;
 import java.util.concurrent.TimeUnit;
 import java.util.logging.LogRecord;
 import java.util.logging.Logger;
 import java.util.logging.Level;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.temporal.Temporal;
 import java.io.IOException;
 import java.nio.file.Path;
 import ucar.nc2.constants.CF;       // String constants are copied by the compiler with no UCAR reference left.
@@ -45,7 +47,7 @@
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.logging.PerformanceLevel;
 import org.apache.sis.util.collection.TreeTable;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.iso.DefaultNameFactory;
 import org.apache.sis.referencing.privy.ReferencingFactoryContainer;
 
@@ -74,6 +76,13 @@
     public static final String FORMAT_NAME = "netCDF";
 
     /**
+     * The locale of data such as number formats, dates and names.
+     * This is used, for example, for the conversion to lower-cases before case-insensitive searches.
+     * This is not the locale for error messages or warnings reported to the user.
+     */
+    public static final Locale DATA_LOCALE = Locale.US;
+
+    /**
      * The path to the netCDF file, or {@code null} if unknown.
      * This is set by netCDF store constructor and shall not be modified afterward.
      * This is used for information purpose only, not for actual reading operation.
@@ -326,7 +335,7 @@
      * @param  name  the name of the attribute to search, or {@code null}.
      * @return the attribute value, or {@code null} if none or unparsable or if the given name was null.
      */
-    public abstract Date dateValue(String name);
+    public abstract Temporal dateValue(String name);
 
     /**
      * Converts the given numerical values to date, using the information provided in the given unit symbol.
@@ -336,15 +345,15 @@
      * @param  values  the values to convert. May contains {@code null} elements.
      * @return the converted values. May contains {@code null} elements.
      */
-    public abstract Date[] numberToDate(String symbol, Number... values);
+    public abstract Temporal[] numberToDate(String symbol, Number... values);
 
     /**
      * Returns the timezone for decoding dates. Currently fixed to UTC.
      *
      * @return the timezone for dates.
      */
-    public TimeZone getTimeZone() {
-        return TimeZone.getTimeZone(StandardDateFormat.UTC);
+    public ZoneId getTimeZone() {
+        return ZoneOffset.UTC;
     }
 
     /**
@@ -508,7 +517,7 @@
         final Logger logger = listeners.getLogger();
         if (logger.isLoggable(level)) {
             final LogRecord record = resources().getLogRecord(level, resourceKey,
-                    getFilename(), time / (double) StandardDateFormat.NANOS_PER_SECOND);
+                    getFilename(), time / (double) Constants.NANOS_PER_SECOND);
             Logging.completeAndLog(logger, caller, method, record);
         }
     }
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
index 95d93cf..6e99cec 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
@@ -21,6 +21,7 @@
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Locale;
+import java.util.TimeZone;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.util.function.Supplier;
@@ -508,11 +509,11 @@
     }
 
     /**
-     * Creates a coordinate reference system by parsing a Well Known Text (WKT) string. The WKT is presumed
-     * to use the GDAL flavor of WKT 1, and warnings are redirected to decoder listeners.
+     * Creates a coordinate reference system by parsing a Well Known Text (WKT) string.
+     * The WKT is presumed to use the GDAL flavor of WKT 1, and warnings are redirected to decoder listeners.
      */
     private static CoordinateReferenceSystem createFromWKT(final Node node, final String wkt) throws ParseException {
-        final WKTFormat f = new WKTFormat(node.getLocale(), node.decoder.getTimeZone());
+        final WKTFormat f = new WKTFormat(Decoder.DATA_LOCALE, TimeZone.getTimeZone(node.decoder.getTimeZone()));
         f.setConvention(org.apache.sis.io.wkt.Convention.WKT1_COMMON_UNITS);
         final CoordinateReferenceSystem crs = (CoordinateReferenceSystem) f.parseObject(wkt);
         final Warnings warnings = f.getWarnings();
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/HYCOM.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/HYCOM.java
index c583ce9..543f9f1 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/HYCOM.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/HYCOM.java
@@ -18,14 +18,14 @@
 
 import java.io.IOException;
 import java.time.Instant;
-import java.util.Locale;
+import java.util.TimeZone;
 import java.util.GregorianCalendar;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.apache.sis.math.Vector;
 import org.apache.sis.measure.Units;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.Constants;
 
 
 /**
@@ -102,7 +102,7 @@
                          */
                         Vector values = variable.read();
                         final double[] times = new double[values.size()];
-                        final GregorianCalendar calendar = new GregorianCalendar(decoder.getTimeZone(), Locale.US);
+                        final GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone(decoder.getTimeZone()), Decoder.DATA_LOCALE);
                         calendar.clear();
                         for (int i=0; i<times.length; i++) {
                             double time = values.doubleValue(i);                            // Date encoded as a double (e.g. 20181017)
@@ -112,7 +112,7 @@
                             int month = (int) (date % 100); date /= 100;
                             calendar.set(Math.toIntExact(date), month - 1, day, 0, 0, 0);
                             date = calendar.getTimeInMillis() - origin;                     // Milliseconds since epoch.
-                            time += date / (double) StandardDateFormat.MILLISECONDS_PER_DAY;
+                            time += date / (double) Constants.MILLISECONDS_PER_DAY;
                             times[i] = time;
                         }
                         variable.setValues(times);
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java
index 58d8b72..06739b6 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java
@@ -20,7 +20,6 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.ArrayList;
-import java.util.Locale;
 import java.util.regex.Pattern;
 import java.io.IOException;
 import java.time.Instant;
@@ -1286,7 +1285,7 @@
      * @param  buffer  the buffer when to append the name of the variable data type.
      */
     public final void writeDataTypeName(final StringBuilder buffer) {
-        buffer.append(getDataType().name().toLowerCase(Locale.US));
+        buffer.append(getDataType().name().toLowerCase(Decoder.DATA_LOCALE));
         final List<Dimension> dimensions = getGridDimensions();
         for (int i=dimensions.size(); --i>=0;) {
             dimensions.get(i).writeLength(buffer);
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java
index 9e1b23e..a91de60 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java
@@ -28,7 +28,6 @@
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.Locale;
 import java.util.regex.Matcher;
 import java.time.DateTimeException;
@@ -37,6 +36,8 @@
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.channels.ReadableByteChannel;
+import java.time.Instant;
+import java.time.temporal.Temporal;
 import javax.measure.UnitConverter;
 import javax.measure.IncommensurableException;
 import javax.measure.format.MeasurementParseException;
@@ -58,6 +59,7 @@
 import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.CollectionsExt;
 import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.TemporalDate;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.collection.TreeTable;
@@ -104,14 +106,6 @@
      */
     private static final Charset NAME_ENCODING = StandardCharsets.UTF_8;
 
-    /**
-     * The locale of dimension, variable and attribute names. This is used for the conversion to
-     * lower-cases before case-insensitive searches.
-     *
-     * @see #findAttribute(String)
-     */
-    static final Locale NAME_LOCALE = Locale.US;
-
     /*
      * NOTE: the names of the static constants below this point match the names used in the Backus-Naur Form (BNF)
      *       definitions in the netCDF Classic and 64-bit Offset Format (1.0) specification (link in class javdoc),
@@ -289,7 +283,7 @@
                 }
             }
         }
-        attributeMap = CollectionsExt.toCaseInsensitiveNameMap(attributes, NAME_LOCALE);
+        attributeMap = CollectionsExt.toCaseInsensitiveNameMap(attributes, Decoder.DATA_LOCALE);
         attributeNames = attributeNames(attributes, attributeMap);
         if (variables != null) {
             this.variables   = variables;
@@ -326,7 +320,7 @@
                 final E e = elements[index];
                 return new AbstractMap.SimpleImmutableEntry<>(e.getName(), e);
             }
-        }, NAME_LOCALE);
+        }, Decoder.DATA_LOCALE);
     }
 
     /**
@@ -631,7 +625,7 @@
                     default: throw malformedHeader();
                 }
             }
-            final Map<String,Object> map = CollectionsExt.toCaseInsensitiveNameMap(attributes, NAME_LOCALE);
+            final Map<String,Object> map = CollectionsExt.toCaseInsensitiveNameMap(attributes, Decoder.DATA_LOCALE);
             variables[j] = new VariableInfo(this, input, name, varDims, map, attributeNames(attributes, map),
                     DataType.valueOf(input.readInt()), input.readInt(), readOffset());
         }
@@ -727,7 +721,7 @@
     protected Dimension findDimension(final String dimName) {
         DimensionInfo dim = dimensionMap.get(dimName);          // Give precedence to exact match before to ignore case.
         if (dim == null) {
-            final String lower = dimName.toLowerCase(ChannelDecoder.NAME_LOCALE);
+            final String lower = dimName.toLowerCase(Decoder.DATA_LOCALE);
             if (lower != dimName) {                             // Identity comparison is okay here.
                 dim = dimensionMap.get(lower);
             }
@@ -744,7 +738,7 @@
     private VariableInfo findVariableInfo(final String name) {
         VariableInfo v = variableMap.get(name);
         if (v == null && name != null) {
-            final String lower = name.toLowerCase(NAME_LOCALE);
+            final String lower = name.toLowerCase(Decoder.DATA_LOCALE);
             // Identity comparison is ok since following check is only an optimization for a common case.
             if (lower != name) {
                 v = variableMap.get(lower);
@@ -807,7 +801,7 @@
              * Identity comparisons performed between String instances below are okay since they
              * are only optimizations for skipping calls to Map.get(Object) in common cases.
              */
-            final String lowerCase = mappedName.toLowerCase(NAME_LOCALE);
+            final String lowerCase = mappedName.toLowerCase(DATA_LOCALE);
             if (lowerCase != mappedName) {
                 value = attributeMap.get(lowerCase);
                 if (value != null) return value;
@@ -866,11 +860,11 @@
      * @return {@inheritDoc}
      */
     @Override
-    public Date dateValue(final String name) {
+    public Temporal dateValue(final String name) {
         final Object value = findAttribute(name);
         if (value instanceof CharSequence) try {
-            return StandardDateFormat.toDate(StandardDateFormat.FORMAT.parse((CharSequence) value));
-        } catch (DateTimeException | ArithmeticException e) {
+            return StandardDateFormat.parseBest((CharSequence) value);
+        } catch (RuntimeException e) {
             listeners.warning(e);
         }
         return null;
@@ -884,16 +878,16 @@
      * @return the converted values. May contain {@code null} elements.
      */
     @Override
-    public Date[] numberToDate(final String symbol, final Number... values) {
-        final Date[] dates = new Date[values.length];
+    public Temporal[] numberToDate(final String symbol, final Number... values) {
+        final var dates = new Instant[values.length];
         final Matcher parts = Variable.TIME_UNIT_PATTERN.matcher(symbol);
         if (parts.matches()) try {
-            final UnitConverter converter = Units.valueOf(parts.group(1)).getConverterToAny(Units.MILLISECOND);
-            final long epoch = StandardDateFormat.toDate(StandardDateFormat.FORMAT.parse(parts.group(2))).getTime();
+            final UnitConverter converter = Units.valueOf(parts.group(1)).getConverterToAny(Units.SECOND);
+            final Instant epoch = StandardDateFormat.parseInstantUTC(parts.group(2));
             for (int i=0; i<values.length; i++) {
                 final Number value = values[i];
                 if (value != null) {
-                    dates[i] = new Date(epoch + Math.round(converter.convert(value.doubleValue())));
+                    dates[i] = TemporalDate.addSeconds(epoch, converter.convert(value.doubleValue()));
                 }
             }
         } catch (IncommensurableException | MeasurementParseException | DateTimeException | ArithmeticException e) {
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/DecoderWrapper.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/DecoderWrapper.java
index d64e8e6..381d8b1 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/DecoderWrapper.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/DecoderWrapper.java
@@ -18,11 +18,12 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.util.Date;
 import java.util.Set;
 import java.util.List;
 import java.util.Formatter;
 import java.util.Collection;
+import java.time.Instant;
+import java.time.temporal.Temporal;
 import ucar.nc2.Group;
 import ucar.nc2.Attribute;
 import ucar.nc2.NetcdfFile;
@@ -318,7 +319,7 @@
      * @return the attribute value, or {@code null} if none or unparsable or if the given name was null.
      */
     @Override
-    public Date dateValue(final String name) {
+    public Temporal dateValue(final String name) {
         if (name != null) {
             for (final Group group : groups) {
                 final Attribute attribute = findAttribute(group, name);
@@ -332,7 +333,7 @@
                             listeners.warning(e);
                             continue;
                         }
-                        return new Date(date.getMillis());
+                        return Instant.ofEpochMilli(date.getMillis());
                     }
                 }
             }
@@ -348,8 +349,8 @@
      * @return the converted values. May contains {@code null} elements.
      */
     @Override
-    public Date[] numberToDate(final String symbol, final Number... values) {
-        final Date[] dates = new Date[values.length];
+    public Temporal[] numberToDate(final String symbol, final Number... values) {
+        final var dates = new Instant[values.length];
         final DateUnit unit;
         try {
             unit = new DateUnit(symbol);
@@ -360,7 +361,7 @@
         for (int i=0; i<values.length; i++) {
             final Number value = values[i];
             if (value != null) {
-                dates[i] = unit.makeDate(value.doubleValue());
+                dates[i] = unit.makeDate(value.doubleValue()).toInstant();
             }
         }
         return dates;
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/base/DecoderTest.java b/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/base/DecoderTest.java
index fb88162..cc73129 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/base/DecoderTest.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/base/DecoderTest.java
@@ -16,7 +16,7 @@
  */
 package org.apache.sis.storage.netcdf.base;
 
-import java.util.Date;
+import java.time.Instant;
 import java.io.IOException;
 import org.apache.sis.storage.DataStoreException;
 import static org.apache.sis.storage.netcdf.AttributeNames.*;
@@ -24,7 +24,6 @@
 // Test dependencies
 import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.*;
-import static org.apache.sis.test.TestUtilities.date;
 
 
 /**
@@ -85,9 +84,9 @@
     @Test
     public void testDateValue() throws IOException, DataStoreException {
         selectDataset(TestData.NETCDF_2D_GEOGRAPHIC);
-        assertAttributeEquals(date("2005-09-22 00:00:00"), DATE_CREATED);
-        assertAttributeEquals(date("2018-05-15 13:00:00"), DATE_MODIFIED);
-        assertAttributeEquals((Date) null,                 DATE_ISSUED);
+        assertAttributeEquals(Instant.parse("2005-09-22T00:00:00Z"), DATE_CREATED);
+        assertAttributeEquals(Instant.parse("2018-05-15T13:00:00Z"), DATE_MODIFIED);
+        assertAttributeEquals((Instant) null, DATE_ISSUED);
     }
 
     /**
@@ -99,14 +98,14 @@
     @Test
     public void testNumberToDate() throws IOException, DataStoreException {
         final Decoder decoder = selectDataset(TestData.NETCDF_2D_GEOGRAPHIC);
-        assertArrayEquals(new Date[] {
-            date("2005-09-22 00:00:00")
+        assertArrayEquals(new Instant[] {
+            Instant.parse("2005-09-22T00:00:00Z")
         }, decoder.numberToDate("hours since 1992-1-1", 120312));
 
-        assertArrayEquals(new Date[] {
-            date("1970-01-09 18:00:00"),
-            date("1969-12-29 06:00:00"),
-            date("1993-04-10 00:00:00")
+        assertArrayEquals(new Instant[] {
+            Instant.parse("1970-01-09T18:00:00Z"),
+            Instant.parse("1969-12-29T06:00:00Z"),
+            Instant.parse("1993-04-10T00:00:00Z")
         }, decoder.numberToDate("days since 1970-01-01T00:00:00Z", 8.75, -2.75, 8500));
     }
 
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/base/TestCase.java b/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/base/TestCase.java
index 892938e..d5ae778 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/base/TestCase.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/base/TestCase.java
@@ -16,9 +16,10 @@
  */
 package org.apache.sis.storage.netcdf.base;
 
-import java.util.Date;
 import java.util.EnumMap;
 import java.util.Iterator;
+import java.time.Instant;
+import java.time.ZoneOffset;
 import java.io.IOException;
 import java.lang.reflect.UndeclaredThrowableException;
 import ucar.nc2.NetcdfFiles;
@@ -27,10 +28,10 @@
 import ucar.nc2.dataset.NetcdfDataset;
 import org.apache.sis.storage.AbstractResource;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.DataStoreMock;
-import org.apache.sis.storage.netcdf.ucar.DecoderWrapper;
-import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.storage.event.StoreListeners;
+import org.apache.sis.storage.netcdf.ucar.DecoderWrapper;
+import org.apache.sis.util.privy.TemporalDate;
+import org.apache.sis.setup.GeometryLibrary;
 
 // Test dependencies
 import org.junit.jupiter.api.AfterAll;
@@ -39,6 +40,7 @@
 import static org.junit.jupiter.api.Assertions.*;
 import org.junit.jupiter.api.parallel.Execution;
 import org.junit.jupiter.api.parallel.ExecutionMode;
+import org.apache.sis.storage.DataStoreMock;
 
 
 /**
@@ -259,7 +261,7 @@
      * @param  attributeName  the name of the attribute to test.
      * @throws IOException if an error occurred while reading the netCDF file.
      */
-    protected final void assertAttributeEquals(final Date expected, final String attributeName) throws IOException {
-        assertEquals(expected, decoder.dateValue(attributeName), attributeName);
+    protected final void assertAttributeEquals(final Instant expected, final String attributeName) throws IOException {
+        assertEquals(expected, TemporalDate.toInstant(decoder.dateValue(attributeName), ZoneOffset.UTC), attributeName);
     }
 }
diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java
index ec94830..b1158d1 100644
--- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java
+++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java
@@ -33,7 +33,7 @@
 import java.math.BigDecimal;
 import org.apache.sis.math.Vector;
 import org.apache.sis.util.Numbers;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.UnmodifiableArrayList;
 
 
@@ -361,8 +361,8 @@
              * `Time.toLocalTime()` does not use sub-second precision.
              * However, some databases provide millisecond precision.
              */
-            final int milli = (int) (time.getTime() % StandardDateFormat.MILLIS_PER_SECOND);
-            return time.toLocalTime().withNano(milli * StandardDateFormat.NANOS_PER_MILLISECOND);
+            final int milli = (int) (time.getTime() % Constants.MILLIS_PER_SECOND);
+            return time.toLocalTime().withNano(milli * Constants.NANOS_PER_MILLISECOND);
         }
     }
 
@@ -440,8 +440,8 @@
             final Time time = source.getTime(columnIndex);
             if (time == null) return null;
             final int offsetMinute = -time.getTimezoneOffset();
-            final int milli = (int) (time.getTime() % StandardDateFormat.MILLIS_PER_SECOND);
-            return time.toLocalTime().withNano(milli * StandardDateFormat.NANOS_PER_MILLISECOND)
+            final int milli = (int) (time.getTime() % Constants.MILLIS_PER_SECOND);
+            return time.toLocalTime().withNano(milli * Constants.NANOS_PER_MILLISECOND)
                     .atOffset(ZoneOffset.ofHoursMinutes(offsetMinute / 60, offsetMinute % 60));
         }
     }
diff --git a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxStreamReader.java b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxStreamReader.java
index 1b73721..ca5b904 100644
--- a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxStreamReader.java
+++ b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxStreamReader.java
@@ -371,7 +371,7 @@
      */
     protected final Date getElementAsDate() throws XMLStreamException {
         final String text = getElementText();
-        return (text != null) ? StandardDateFormat.toDate(StandardDateFormat.FORMAT.parse(text)) : null;
+        return (text == null) ? null : Date.from(StandardDateFormat.parseInstantUTC(text));
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractGridCoverageResource.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractGridCoverageResource.java
index 46a0e93..a0abd3b 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractGridCoverageResource.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractGridCoverageResource.java
@@ -37,7 +37,7 @@
 import org.apache.sis.measure.AngleFormat;
 import org.apache.sis.util.logging.PerformanceLevel;
 import org.apache.sis.io.stream.IOUtilities;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.Constants;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.internal.Resources;
 
@@ -203,7 +203,7 @@
             final Locale locale = listeners.getLocale();
             final Object[] parameters = new Object[6];
             parameters[0] = IOUtilities.filename(file != null ? file : listeners.getSourceName());
-            parameters[5] = nanos / (double) StandardDateFormat.NANOS_PER_SECOND;
+            parameters[5] = nanos / (double) Constants.NANOS_PER_SECOND;
             domain.getGeographicExtent().ifPresentOrElse((box) -> {
                 final AngleFormat f = new AngleFormat(locale);
                 double min = box.getSouthBoundLatitude();
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/LegalSymbols.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/LegalSymbols.java
index 8450ecd..93dbbdb 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/LegalSymbols.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/LegalSymbols.java
@@ -27,7 +27,7 @@
 import org.apache.sis.metadata.iso.citation.DefaultCitation;
 import org.apache.sis.metadata.iso.citation.DefaultCitationDate;
 import org.apache.sis.metadata.iso.constraint.DefaultLegalConstraints;
-import static org.apache.sis.util.privy.StandardDateFormat.MILLISECONDS_PER_DAY;
+import static org.apache.sis.util.privy.Constants.MILLISECONDS_PER_DAY;
 
 // Specific to the main and geoapi-3.1 branches:
 import org.apache.sis.metadata.iso.citation.DefaultResponsibleParty;
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java
index 82c9f69..a441828 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java
@@ -71,7 +71,7 @@
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.privy.CollectionsExt;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.Strings;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.metadata.ModifiableMetadata;
@@ -1125,9 +1125,9 @@
      * @param  type   the type of the date to add, or {@code null} if none (not legal but tolerated).
      * @param  scope  whether the date applies to data, to metadata or to both.
      *
-     * @see #addAcquisitionTime(Date)
+     * @see #addAcquisitionTime(Temporal)
      */
-    public final void addCitationDate(final Date date, final DateType type, final Scope scope) {
+    public final void addCitationDate(final Temporal date, final DateType type, final Scope scope) {
         if (date != null) {
             final var cd = new DefaultCitationDate(date, type);
             if (scope != Scope.RESOURCE) addEarliest(metadata().getDateInfo(), cd, type);
@@ -1800,25 +1800,10 @@
      *
      * @param  startTime  when the data begins, or {@code null} if unbounded.
      * @param  endTime    when the data ends, or {@code null} if unbounded.
+     *
+     * @see #addAcquisitionTime(Temporal)
      */
     public final void addTemporalExtent(final Temporal startTime, final Temporal endTime) {
-        addTemporalExtent(StandardDateFormat.toDate(startTime), StandardDateFormat.toDate(endTime));
-    }
-
-    /**
-     * Adds a temporal extent covered by the data.
-     * Storage location is:
-     *
-     * <ul>
-     *   <li>{@code metadata/identificationInfo/extent/temporalElement}</li>
-     * </ul>
-     *
-     * @param  startTime  when the data begins, or {@code null} if unbounded.
-     * @param  endTime    when the data ends, or {@code null} if unbounded.
-     *
-     * @see #addAcquisitionTime(Date)
-     */
-    public final void addTemporalExtent(final Date startTime, final Date endTime) {
         if (startTime != null || endTime != null) {
             final var t = new DefaultTemporalExtent();
             t.setBounds(startTime, endTime);
@@ -2051,7 +2036,7 @@
     public final void addTemporalResolution(final double duration) {
         if (Double.isFinite(duration)) {
             addIfNotPresent(identification().getTemporalResolutions(),
-                    Duration.ofMillis(Math.round(duration * StandardDateFormat.MILLISECONDS_PER_DAY)));
+                    Duration.ofNanos(Math.round(duration * Constants.NANOSECONDS_PER_DAY)));
         }
     }
 
@@ -2701,9 +2686,9 @@
      *
      * @param  time  the acquisition time, or {@code null} for no-operation.
      *
-     * @see #addTemporalExtent(Date, Date)
+     * @see #addTemporalExtent(Temporal, Temporal)
      */
-    public final void addAcquisitionTime(final Date time) {
+    public final void addAcquisitionTime(final Temporal time) {
         if (time != null) {
             final var event = new DefaultEvent();
             event.setContext(Context.ACQUISITION);
@@ -2729,15 +2714,15 @@
      * @param  endTime    end time, or {@code null} if unknown.
      */
     public final void addAcquisitionTime(final Instant startTime, final Instant endTime) {
-        final Date time;
+        final Temporal time;
         if (startTime == null) {
             if (endTime == null) return;
-            time = Date.from(endTime);
+            time = endTime;
         } else if (endTime == null) {
-            time = Date.from(startTime);
+            time = startTime;
         } else {
             // Divide by 2 before to add in order to avoid overflow.
-            time = new Date((startTime.toEpochMilli() >> 1) + (endTime.toEpochMilli() >> 1));
+            time = Instant.ofEpochMilli((startTime.toEpochMilli() >> 1) + (endTime.toEpochMilli() >> 1));
         }
         addAcquisitionTime(time);
     }
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
index 37e92b8..1b818fb 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
@@ -19,7 +19,6 @@
 import java.util.List;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Date;
 import java.util.Locale;
 import java.util.Optional;
 import java.util.stream.Stream;
@@ -448,7 +447,7 @@
                     temporal = TimeEncoding.DEFAULT.crs();
                     timeEncoding = TimeEncoding.ABSOLUTE;
                 } else {
-                    temporal = builder.createTemporalCRS(Date.from(startTime), timeUnit);
+                    temporal = builder.createTemporalCRS(startTime, timeUnit);
                     timeEncoding = new TimeEncoding(temporal.getDatum(), timeUnit);
                 }
                 components[count++] = temporal;
@@ -480,8 +479,8 @@
             envelope.setRange(i, lowerCorner[i], upperCorner[i]);
         }
         if (startTime != null) {
-            envelope.setRange(spatialDimensionCount, timeEncoding.toCRS(startTime.toEpochMilli()),
-                    (endTime == null) ? Double.NaN : timeEncoding.toCRS(endTime.toEpochMilli()));
+            envelope.setRange(spatialDimensionCount, timeEncoding.toCRS(startTime),
+                    (endTime == null) ? Double.NaN : timeEncoding.toCRS(endTime));
         }
         this.spatialDimensionCount = (short) spatialDimensionCount;
         return envelope;
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/TimeEncoding.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/TimeEncoding.java
index 3458095..8fe13bc 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/TimeEncoding.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/TimeEncoding.java
@@ -17,11 +17,13 @@
 package org.apache.sis.storage.csv;
 
 import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import javax.measure.Unit;
 import javax.measure.quantity.Time;
 import org.opengis.referencing.datum.TemporalDatum;
 import org.apache.sis.converter.SurjectiveConverter;
 import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.TemporalDate;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.measure.Units;
 
@@ -49,12 +51,12 @@
     };
 
     /**
-     * Date of value zero on the time axis, in milliseconds since January 1st 1970 at midnight UTC.
+     * Date of value zero on the time axis.
      */
-    private final long origin;
+    private final Instant origin;
 
     /**
-     * Number of milliseconds between two consecutive integer values on the time axis.
+     * Number of seconds between two consecutive integer values on the time axis.
      */
     private final double interval;
 
@@ -62,8 +64,8 @@
      * Creates a new time encoding.
      */
     TimeEncoding(final TemporalDatum datum, final Unit<Time> unit) {
-        this.origin   = datum.getOrigin().getTime();
-        this.interval = unit.getConverterTo(Units.MILLISECOND).convert(1);
+        this.origin   = TemporalDate.toInstant(datum.getOrigin());
+        this.interval = unit.getConverterTo(Units.SECOND).convert(1);
     }
 
     /**
@@ -90,23 +92,16 @@
      */
     @Override
     public Instant apply(final String time) {
-        final double value = Double.parseDouble(time) * interval;
-        final long millis = Math.round(value);
-        return Instant.ofEpochMilli(millis + origin)
-                      .plusNanos(Math.round((value - millis) * StandardDateFormat.NANOS_PER_MILLISECOND));
-        /*
-         * Performance note: the call to .plusNano(…) will usually return the same 'Instant' instance
-         * (without creating new object) since the time granularity is rarely finer than milliseconds.
-         */
+        return TemporalDate.addSeconds(origin, Double.parseDouble(time) * interval);
     }
 
     /**
      * Converts the given timestamp to the values used in the temporal coordinate reference system.
      *
-     * @param  time  number of milliseconds elapsed since January 1st, 1970 midnight UTC.
+     * @param  time  instant to convert.
      * @return the value to use with the temporal coordinate reference system.
      */
-    final double toCRS(final long time) {
-        return (time - origin) / interval;
+    final double toCRS(final Instant time) {
+        return origin.until(time, ChronoUnit.SECONDS) / interval;
     }
 }
diff --git a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/test/CoverageReadConsistency.java b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/test/CoverageReadConsistency.java
index a5744ff..3003ead 100644
--- a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/test/CoverageReadConsistency.java
+++ b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/test/CoverageReadConsistency.java
@@ -33,7 +33,7 @@
 import org.apache.sis.storage.RasterLoadingStrategy;
 import org.apache.sis.util.Workaround;
 import org.apache.sis.util.ArraysExt;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.Numerics;
 import org.apache.sis.image.PixelIterator;
 import org.apache.sis.math.Statistics;
@@ -463,7 +463,7 @@
                 break;
             }
             if (durations != null) {
-                durations.accept((System.nanoTime() - startTime) / (double) StandardDateFormat.NANOS_PER_MILLISECOND);
+                durations.accept((System.nanoTime() - startTime) / (double) Constants.NANOS_PER_MILLISECOND);
             }
         }
         /*
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/CompoundFormat.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/CompoundFormat.java
index d45660d..5d912fa 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/CompoundFormat.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/CompoundFormat.java
@@ -48,7 +48,7 @@
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.privy.MetadataServices;
 import org.apache.sis.util.privy.LocalizedParseException;
-import static org.apache.sis.util.privy.StandardDateFormat.UTC;
+import static org.apache.sis.util.privy.Constants.UTC;
 
 
 /**
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/RangeFormat.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/RangeFormat.java
index 6bf6a90..30f8441 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/RangeFormat.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/RangeFormat.java
@@ -43,7 +43,7 @@
 import org.apache.sis.util.UnconvertibleObjectException;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.privy.LocalizedParseException;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.TemporalDate;
 import org.apache.sis.util.privy.Numerics;
 
 
@@ -362,8 +362,8 @@
             elementFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale);
             unitFormat    = null;
         } else if (Temporal.class.isAssignableFrom(elementType)) {
-            final FormatStyle dateStyle = StandardDateFormat.hasDateFields(elementType) ? FormatStyle.SHORT : null;
-            final FormatStyle timeStyle = StandardDateFormat.hasTimeFields(elementType) ? FormatStyle.SHORT : null;
+            final FormatStyle dateStyle = TemporalDate.hasDateFields(elementType) ? FormatStyle.SHORT : null;
+            final FormatStyle timeStyle = TemporalDate.hasTimeFields(elementType) ? FormatStyle.SHORT : null;
             elementFormat = new DateTimeFormatterBuilder().appendLocalized(dateStyle, timeStyle).toFormatter(locale).toFormat();
             unitFormat    = null;
         } else {
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitNames.properties b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitNames.properties
index 7cf0da8..ade411d 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitNames.properties
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitNames.properties
@@ -47,6 +47,7 @@
 ms=millisecond
 N=newton
 nm=nanometre
+ns=nanosecond
 Pa=pascal
 ppm=parts per million
 psu=practical salinity unit
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitNames_fr.properties b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitNames_fr.properties
index bb1a4cf..ceb0cce 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitNames_fr.properties
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitNames_fr.properties
@@ -27,6 +27,7 @@
 mm=millim\u00e8tre
 ms=milliseconde
 nm=nanom\u00e8tre
+ns=nanoseconde
 ppm=parties par million
 rad\u2215s=radians par seconde
 s=seconde
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/Units.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/Units.java
index 68954b7..a5405d3 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/Units.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/Units.java
@@ -35,6 +35,8 @@
 import static org.apache.sis.measure.UnitRegistry.IMPERIAL;
 import static org.apache.sis.measure.UnitRegistry.OTHER;
 import static org.apache.sis.measure.UnitRegistry.PREFIXABLE;
+import static org.apache.sis.util.privy.Constants.SECONDS_PER_DAY;
+import static org.apache.sis.util.privy.Constants.MILLIS_PER_TROPICAL_YEAR;
 
 
 /**
@@ -56,7 +58,7 @@
  *   <tr><td style="padding-top:15px" colspan="4"><b>Fundamental:</b></td></tr>
  *   <tr><td>{@link Length}</td>            <td>(L)</td> <td>{@link #METRE}</td>    <td>{@link #CENTIMETRE}, {@link #KILOMETRE}, {@link #NAUTICAL_MILE}, {@link #STATUTE_MILE}, {@link #FOOT}</td></tr>
  *   <tr><td>{@link Mass}</td>              <td>(M)</td> <td>{@link #KILOGRAM}</td> <td></td></tr>
- *   <tr><td>{@link Time}</td>              <td>(T)</td> <td>{@link #SECOND}</td>   <td>{@link #MILLISECOND}, {@link #MINUTE}, {@link #HOUR}, {@link #DAY}, {@link #WEEK}, {@link #TROPICAL_YEAR}</td></tr>
+ *   <tr><td>{@link Time}</td>              <td>(T)</td> <td>{@link #SECOND}</td>   <td>{@link #NANOSECOND}, {@link #MILLISECOND}, {@link #MINUTE}, {@link #HOUR}, {@link #DAY}, {@link #WEEK}, {@link #TROPICAL_YEAR}</td></tr>
  *   <tr><td>{@link ElectricCurrent}</td>   <td>(I)</td> <td>{@link #AMPERE}</td>   <td></td></tr>
  *   <tr><td>{@link Temperature}</td>       <td>(Θ)</td> <td>{@link #KELVIN}</td>   <td>{@link #CELSIUS}, {@link #FAHRENHEIT}</td></tr>
  *   <tr><td>{@link AmountOfSubstance}</td> <td>(N)</td> <td>{@link #MOLE}</td>     <td></td></tr>
@@ -447,6 +449,25 @@
     public static final Unit<Angle> GRAD;
 
     /**
+     * Unit of measurement defined as 10<sup>-9</sup> seconds (1 ms).
+     * This unit is useful for inter-operability with various methods from the standard Java library.
+     * The {@linkplain ConventionalUnit#getSystemUnit() system unit} is {@link #SECOND}
+     * and the unlocalized name is “nanosecond”.
+     *
+     * <table class="compact" style="margin-left:30px; line-height:1.25">
+     *   <caption>Related units</caption>
+     *   <tr><td>SI time units:</td> <td style="word-spacing:1em"><u>{@code NANOSECOND}</u>, {@link #MILLISECOND}, <b>{@link #SECOND}</b>.</td></tr>
+     *   <tr><td>Non-SI units:</td>  <td style="word-spacing:1em">{@link #MINUTE}, {@link #HOUR}, {@link #DAY}, {@link #WEEK}, {@link #TROPICAL_YEAR}.</td></tr>
+     *   <tr><td>Derived units:</td> <td style="word-spacing:1em">{@link #METRES_PER_SECOND}, {@link #HERTZ}, {@link #BECQUEREL}.</td></tr>
+     * </table>
+     *
+     * @see java.util.concurrent.TimeUnit#NANOSECONDS
+     *
+     * @since 1.5
+     */
+    public static final Unit<Time> NANOSECOND;
+
+    /**
      * Unit of measurement defined as 10<sup>-3</sup> seconds (1 ms).
      * This unit is useful for inter-operability with various methods from the standard Java library.
      * The {@linkplain ConventionalUnit#getSystemUnit() system unit} is {@link #SECOND}
@@ -454,7 +475,7 @@
      *
      * <table class="compact" style="margin-left:30px; line-height:1.25">
      *   <caption>Related units</caption>
-     *   <tr><td>SI time units:</td> <td style="word-spacing:1em"><u>{@code MILLISECOND}</u>, <b>{@link #SECOND}</b>.</td></tr>
+     *   <tr><td>SI time units:</td> <td style="word-spacing:1em"><u>{@link #NANOSECOND}, {@code MILLISECOND}</u>, <b>{@link #SECOND}</b>.</td></tr>
      *   <tr><td>Non-SI units:</td>  <td style="word-spacing:1em">{@link #MINUTE}, {@link #HOUR}, {@link #DAY}, {@link #WEEK}, {@link #TROPICAL_YEAR}.</td></tr>
      *   <tr><td>Derived units:</td> <td style="word-spacing:1em">{@link #METRES_PER_SECOND}, {@link #HERTZ}, {@link #BECQUEREL}.</td></tr>
      * </table>
@@ -472,7 +493,7 @@
      *
      * <table class="compact" style="margin-left:30px; line-height:1.25">
      *   <caption>Related units</caption>
-     *   <tr><td>SI time units:</td> <td style="word-spacing:1em">{@link #MILLISECOND}, <u><b>{@link #SECOND}</b></u>.</td></tr>
+     *   <tr><td>SI time units:</td> <td style="word-spacing:1em">{@link #NANOSECOND}, {@link #MILLISECOND}, <u><b>{@link #SECOND}</b></u>.</td></tr>
      *   <tr><td>Non-SI units:</td>  <td style="word-spacing:1em">{@link #MINUTE}, {@link #HOUR}, {@link #DAY}, {@link #WEEK}, {@link #TROPICAL_YEAR}.</td></tr>
      *   <tr><td>Derived units:</td> <td style="word-spacing:1em">{@link #METRES_PER_SECOND}, {@link #HERTZ}, {@link #BECQUEREL}.</td></tr>
      * </table>
@@ -488,7 +509,7 @@
      *
      * <table class="compact" style="margin-left:30px; line-height:1.25">
      *   <caption>Related units</caption>
-     *   <tr><td>SI time units:</td> <td style="word-spacing:1em">{@link #MILLISECOND}, <b>{@link #SECOND}</b>.</td></tr>
+     *   <tr><td>SI time units:</td> <td style="word-spacing:1em">{@link #NANOSECOND}, {@link #MILLISECOND}, <b>{@link #SECOND}</b>.</td></tr>
      *   <tr><td>Non-SI units:</td>  <td style="word-spacing:1em"><u>{@code MINUTE}</u>, {@link #HOUR}, {@link #DAY}, {@link #WEEK}, {@link #TROPICAL_YEAR}.</td></tr>
      *   <tr><td>Derived units:</td> <td style="word-spacing:1em">{@link #METRES_PER_SECOND}, {@link #HERTZ}, {@link #BECQUEREL}.</td></tr>
      * </table>
@@ -504,7 +525,7 @@
      *
      * <table class="compact" style="margin-left:30px; line-height:1.25">
      *   <caption>Related units</caption>
-     *   <tr><td>SI time units:</td> <td style="word-spacing:1em">{@link #MILLISECOND}, <b>{@link #SECOND}</b>.</td></tr>
+     *   <tr><td>SI time units:</td> <td style="word-spacing:1em">{@link #NANOSECOND}, {@link #MILLISECOND}, <b>{@link #SECOND}</b>.</td></tr>
      *   <tr><td>Non-SI units:</td>  <td style="word-spacing:1em">{@link #MINUTE}, <u>{@code HOUR}</u>, {@link #DAY}, {@link #WEEK}, {@link #TROPICAL_YEAR}.</td></tr>
      *   <tr><td>Derived units:</td> <td style="word-spacing:1em">{@link #KILOMETRES_PER_HOUR}, {@link #HERTZ}, {@link #BECQUEREL}.</td></tr>
      * </table>
@@ -520,7 +541,7 @@
      *
      * <table class="compact" style="margin-left:30px; line-height:1.25">
      *   <caption>Related units</caption>
-     *   <tr><td>SI time units:</td> <td style="word-spacing:1em">{@link #MILLISECOND}, <b>{@link #SECOND}</b>.</td></tr>
+     *   <tr><td>SI time units:</td> <td style="word-spacing:1em">{@link #NANOSECOND}, {@link #MILLISECOND}, <b>{@link #SECOND}</b>.</td></tr>
      *   <tr><td>Non-SI units:</td>  <td style="word-spacing:1em">{@link #MINUTE}, {@link #HOUR}, <u>{@code DAY}</u>, {@link #WEEK}, {@link #TROPICAL_YEAR}.</td></tr>
      *   <tr><td>Derived units:</td> <td style="word-spacing:1em">{@link #KILOMETRES_PER_HOUR}, {@link #HERTZ}, {@link #BECQUEREL}.</td></tr>
      * </table>
@@ -536,7 +557,7 @@
      *
      * <table class="compact" style="margin-left:30px; line-height:1.25">
      *   <caption>Related units</caption>
-     *   <tr><td>SI time units:</td> <td style="word-spacing:1em">{@link #MILLISECOND}, <b>{@link #SECOND}</b>.</td></tr>
+     *   <tr><td>SI time units:</td> <td style="word-spacing:1em">{@link #NANOSECOND}, {@link #MILLISECOND}, <b>{@link #SECOND}</b>.</td></tr>
      *   <tr><td>Non-SI units:</td>  <td style="word-spacing:1em">{@link #MINUTE}, {@link #HOUR}, {@link #DAY}, <u>{@link #WEEK}</u>, {@link #TROPICAL_YEAR}.</td></tr>
      *   <tr><td>Derived units:</td> <td style="word-spacing:1em">{@link #KILOMETRES_PER_HOUR}, {@link #HERTZ}, {@link #BECQUEREL}.</td></tr>
      * </table>
@@ -554,7 +575,7 @@
      *
      * <table class="compact" style="margin-left:30px; line-height:1.25">
      *   <caption>Related units</caption>
-     *   <tr><td>SI time units:</td> <td style="word-spacing:1em">{@link #MILLISECOND}, <b>{@link #SECOND}</b>.</td></tr>
+     *   <tr><td>SI time units:</td> <td style="word-spacing:1em">{@link #NANOSECOND}, {@link #MILLISECOND}, <b>{@link #SECOND}</b>.</td></tr>
      *   <tr><td>Non-SI units:</td>  <td style="word-spacing:1em">{@link #MINUTE}, {@link #HOUR}, {@link #DAY}, {@link #WEEK}, <u>{@code TROPICAL_YEAR}</u>.</td></tr>
      *   <tr><td>Derived units:</td> <td style="word-spacing:1em">{@link #KILOMETRES_PER_HOUR}, {@link #HERTZ}, {@link #BECQUEREL}.</td></tr>
      * </table>
@@ -1256,12 +1277,13 @@
          */
         s.related(5);
         SECOND         = s;
-        MILLISECOND    = add(s, milli,                                      "ms",  SI,       (short) 0);
-        MINUTE         = add(s, LinearConverter.scale(         60,      1), "min", ACCEPTED, (short) 0);
-        HOUR           = add(s, LinearConverter.scale(      60*60,      1), "h",   ACCEPTED, (short) 0);
-        DAY            = add(s, LinearConverter.scale(   24*60*60,      1), "d",   ACCEPTED, (short) 0);
-        WEEK           = add(s, LinearConverter.scale( 7*24*60*60,      1), "wk",  OTHER,    (short) 0);
-        TROPICAL_YEAR  = add(s, LinearConverter.scale(31556925445.0, 1000), "a",   OTHER,    (short) 1029);
+        NANOSECOND     = add(s, nano,                                                  "ns",  SI,       (short) 0);
+        MILLISECOND    = add(s, milli,                                                 "ms",  SI,       (short) 0);
+        MINUTE         = add(s, LinearConverter.scale(                      60,    1), "min", ACCEPTED, (short) 0);
+        HOUR           = add(s, LinearConverter.scale(                   60*60,    1), "h",   ACCEPTED, (short) 0);
+        DAY            = add(s, LinearConverter.scale(         SECONDS_PER_DAY,    1), "d",   ACCEPTED, (short) 0);
+        WEEK           = add(s, LinearConverter.scale(       7*SECONDS_PER_DAY,    1), "wk",  OTHER,    (short) 0);
+        TROPICAL_YEAR  = add(s, LinearConverter.scale(MILLIS_PER_TROPICAL_YEAR, 1000), "a",   OTHER,    (short) 1029);
         /*
          * All Unit<Speed>, Unit<Acceleration>, Unit<AngularVelocity> and Unit<ScaleRateOfChange>.
          * The `unityPerSecond` unit is not added to the registry because it is specific to the EPSG database,
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK23.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK23.java
new file mode 100644
index 0000000..a29acac
--- /dev/null
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK23.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.pending.jdk;
+
+import java.time.Duration;
+import java.time.Instant;
+
+
+/**
+ * Place holder for some functionalities defined in a JDK more recent than Java 11.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class JDK23 {
+    /**
+     * Do not allow instantiation of this class.
+     */
+    private JDK23() {
+    }
+
+    /**
+     * Placeholder for {@code Instant.until(Instant)}.
+     * The method provided in Java 23 is optimized compared to the more generic {@code Duration.between} method.
+     * The purpose of this placeholder is only for remembering to do the substitution.
+     *
+     * @param  start the start time.
+     * @param  endExclusive  the end time.
+     * @return duration between the two times.
+     */
+    public static Duration until(Instant start, Instant endExclusive) {
+        return Duration.between(start, endExclusive);
+    }
+}
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/About.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/About.java
index 30b539f..f004670 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/About.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/About.java
@@ -59,7 +59,7 @@
 import org.apache.sis.system.DataDirectory;
 import static org.apache.sis.util.collection.TableColumn.NAME;
 import static org.apache.sis.util.collection.TableColumn.VALUE_AS_TEXT;
-import static org.apache.sis.util.privy.StandardDateFormat.UTC;
+import static org.apache.sis.util.privy.Constants.UTC;
 
 
 /**
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/MonolineFormatter.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/MonolineFormatter.java
index 6cb9e9d..976029c 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/MonolineFormatter.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/MonolineFormatter.java
@@ -42,7 +42,7 @@
 import org.apache.sis.util.internal.AutoMessageFormat;
 import org.apache.sis.io.IO;
 import org.apache.sis.io.LineAppender;
-import static org.apache.sis.util.privy.StandardDateFormat.UTC;
+import static org.apache.sis.util.privy.Constants.UTC;
 
 
 /**
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/Constants.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/Constants.java
index f4a975c..eb93271 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/Constants.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/Constants.java
@@ -50,6 +50,54 @@
     public static final byte DEFAULT_INDENTATION = 2;
 
     /**
+     * The length of a day in number of seconds.
+     * Can be casted to {@code float} with exact precision.
+     */
+    public static final int SECONDS_PER_DAY = 24*60*60;
+
+    /**
+     * The length of a day in number of milliseconds.
+     * Can be casted to {@code float} with exact precision.
+     */
+    public static final int MILLISECONDS_PER_DAY = SECONDS_PER_DAY * 1000;
+
+    /**
+     * The length of a day in number of nanoseconds.
+     */
+    public static final long NANOSECONDS_PER_DAY = MILLISECONDS_PER_DAY * (long) 1_000_000;
+
+    /**
+     * Number of milliseconds in one second.
+     * Can be casted to {@code float} with exact precision.
+     */
+    public static final int MILLIS_PER_SECOND = 1000;
+
+    /**
+     * Number of nanoseconds in one millisecond.
+     * Can be casted to {@code float} with exact precision.
+     */
+    public static final int NANOS_PER_MILLISECOND = 1000_000;
+
+    /**
+     * Number of nanoseconds in one second.
+     * Can be casted to {@code float} with exact precision.
+     */
+    public static final int NANOS_PER_SECOND = 1000_000_000;
+
+    /**
+     * Length of a year as defined by the International Union of Geological Sciences (IUGS), in milliseconds.
+     * This is the unit of measurement used in EPSG geodetic dataset (EPSG:1029).
+     */
+    public static final long MILLIS_PER_TROPICAL_YEAR = 31556925445L;
+
+    /**
+     * The {@value} timezone ID.
+     *
+     * @see java.time.ZoneOffset#UTC
+     */
+    public static final String UTC = "UTC";
+
+    /**
      * The {@value} protocol.
      */
     public static final String HTTP = "http", HTTPS = "https";
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/DoubleDouble.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/DoubleDouble.java
index 94efbb8..15f522b 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/DoubleDouble.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/DoubleDouble.java
@@ -17,6 +17,7 @@
 package org.apache.sis.util.privy;
 
 import java.util.Arrays;
+import java.time.Duration;
 import java.math.BigInteger;
 import java.math.BigDecimal;
 import java.math.MathContext;
@@ -257,6 +258,16 @@
     }
 
     /**
+     * Returns an instance for the given duration in nanoseconds.
+     *
+     * @param  value  the duration to convert.
+     * @return the given duration, in nanoseconds.
+     */
+    public static DoubleDouble of(final Duration value) {
+        return of(value.getSeconds()).multiply(Constants.NANOS_PER_SECOND).add(value.getNano());
+    }
+
+    /**
      * Returns an instance for the given integer.
      *
      * @param  value  the integer value to wrap in a {@code DoubleDouble}.
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/StandardDateFormat.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/StandardDateFormat.java
index 8df9765..6c4ee5e 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/StandardDateFormat.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/StandardDateFormat.java
@@ -27,14 +27,12 @@
 import java.text.ParseException;
 import java.time.DateTimeException;
 import java.time.Instant;
-import java.time.LocalTime;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.OffsetDateTime;
 import java.time.OffsetTime;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
 import java.time.temporal.Temporal;
 import java.time.temporal.ChronoField;
 import java.time.temporal.TemporalQuery;
@@ -67,13 +65,6 @@
 @SuppressWarnings("serial")     // Not intended to be serialized.
 public final class StandardDateFormat extends DateFormat {
     /**
-     * The {@value} timezone ID.
-     *
-     * @see ZoneOffset#UTC
-     */
-    public static final String UTC = "UTC";
-
-    /**
      * Midnight (00:00) UTC.
      */
     private static final OffsetTime MIDNIGHT = OffsetTime.of(0, 0, 0, 0, ZoneOffset.UTC);
@@ -218,92 +209,6 @@
     }
 
     /**
-     * The length of a day in number of milliseconds.
-     * Can be casted to {@code float} with exact precision.
-     */
-    public static final int MILLISECONDS_PER_DAY = 24*60*60*1000;
-
-    /**
-     * Number of milliseconds in one second.
-     * Can be casted to {@code float} with exact precision.
-     */
-    public static final int MILLIS_PER_SECOND = 1000;
-
-    /**
-     * Number of nanoseconds in one millisecond.
-     * Can be casted to {@code float} with exact precision.
-     */
-    public static final int NANOS_PER_MILLISECOND = 1000_000;
-
-    /**
-     * Number of nanoseconds in one second.
-     * Can be casted to {@code float} with exact precision.
-     */
-    public static final int NANOS_PER_SECOND = 1000_000_000;
-
-    /**
-     * Converts the given temporal object into a date.
-     * The given temporal object is typically the value parsed by {@link #FORMAT}.
-     *
-     * @param  temporal  the temporal object to convert, or {@code null}.
-     * @return the legacy date for the given temporal object, or {@code null} if the argument was null.
-     * @throws DateTimeException if a value for the field cannot be obtained.
-     * @throws ArithmeticException if the number of milliseconds is too large.
-     */
-    public static Date toDate(final TemporalAccessor temporal) {
-        if (temporal == null) {
-            return null;
-        }
-        long millis;
-        if (temporal instanceof Instant) {
-            millis = ((Instant) temporal).toEpochMilli();
-        } else if (temporal.isSupported(ChronoField.INSTANT_SECONDS)) {
-            millis = Math.multiplyExact(temporal.getLong(ChronoField.INSTANT_SECONDS), 1000);
-            millis = Math.addExact(millis, temporal.getLong(ChronoField.NANO_OF_SECOND) / 1000000);
-        } else {
-            // Note that the timezone may be unknown here. We assume UTC.
-            millis = Math.multiplyExact(temporal.getLong(ChronoField.EPOCH_DAY), MILLISECONDS_PER_DAY);
-            if (temporal.isSupported(ChronoField.MILLI_OF_DAY)) {
-                millis = Math.addExact(millis, temporal.getLong(ChronoField.MILLI_OF_DAY));
-            }
-        }
-        return new Date(millis);
-    }
-
-    /**
-     * Returns {@code true} if objects of the given class have day, month and hour fields.
-     * This method is defined here for having a single class where to concentrate such heuristic rules.
-     * Note that {@link Instant} does not have date fields.
-     *
-     * @param  date  class of object to test (may be {@code null}).
-     * @return whether the given class is {@link LocalDate} or one of the classes with date + time.
-     *         This list may be expanded in future versions.
-     */
-    public static boolean hasDateFields(final Class<?> date) {
-        return date == LocalDate.class
-            || date == LocalDateTime.class
-            || date == OffsetDateTime.class
-            || date == ZonedDateTime.class;
-    }
-
-    /**
-     * Returns {@code true} if objects of the given class have time fields.
-     * This method is defined here for having a single class where to concentrate such heuristic rules.
-     * Note that {@link Instant} does not have hour fields.
-     *
-     * @param  date  class of object to test (may be {@code null}).
-     * @return whether the given class is {@link LocalTime}, {@link OffsetTime} or one of the classes with date + time.
-     *         This list may be expanded in future versions.
-     */
-    public static boolean hasTimeFields(final Class<?> date) {
-        return date == LocalTime.class
-            || date == OffsetTime.class
-            || date == LocalDateTime.class
-            || date == OffsetDateTime.class
-            || date == ZonedDateTime.class;
-    }
-
-    /**
      * The {@code java.time} parser and formatter. This is usually the {@link #FORMAT} instance
      * unless a different locale or timezone has been specified.
      */
@@ -341,7 +246,7 @@
      */
     public StandardDateFormat(final Locale locale, final TimeZone zone) {
         this(locale);
-        if (!UTC.equals(zone.getID())) {
+        if (!Constants.UTC.equals(zone.getID())) {
             setTimeZone(zone);
         }
     }
@@ -375,14 +280,21 @@
     }
 
     /**
+     * Returns the zone, or UTC if unspecified.
+     */
+    private ZoneId getZone() {
+        final ZoneId zone = format.getZone();
+        return (zone != null) ? zone : ZoneOffset.UTC;
+    }
+
+    /**
      * Returns the timezone used for formatting instants.
      *
      * @return the timezone.
      */
     @Override
     public final TimeZone getTimeZone() {
-        final ZoneId zone = format.getZone();
-        return TimeZone.getTimeZone(zone != null ? zone : ZoneOffset.UTC);
+        return TimeZone.getTimeZone(getZone());
     }
 
     /**
@@ -430,11 +342,7 @@
      */
     @Override
     public StringBuffer format(final Date date, final StringBuffer toAppendTo, final FieldPosition pos) {
-        ZoneId zone = format.getZone();
-        if (zone == null) {
-            zone = ZoneOffset.UTC;
-        }
-        final LocalDateTime dt = LocalDateTime.ofInstant(date.toInstant(), zone);
+        final LocalDateTime dt = LocalDateTime.ofInstant(date.toInstant(), getZone());
         TemporalAccessor value = dt;
         if (dt.getHour() == 0 && dt.getMinute() == 0 && dt.getSecond() == 0 && dt.getNano() == 0) {
             value = dt.toLocalDate();
@@ -454,7 +362,7 @@
     @Override
     public Date parse(final String text, final ParsePosition position) {
         try {
-            return toDate(format.parse(text, position));
+            return Date.from(TemporalDate.toInstant(format.parse(text, position), getZone()));
         } catch (DateTimeException | ArithmeticException e) {
             position.setErrorIndex(getErrorIndex(e, position));
             return null;
@@ -471,8 +379,8 @@
     @Override
     public Date parse(final String text) throws ParseException {
         try {
-            return toDate(format.parse(toISO(text, 0, text.length())));
-        } catch (DateTimeException | ArithmeticException e) {
+            return Date.from(TemporalDate.toInstant(format.parse(toISO(text, 0, text.length())), getZone()));
+        } catch (RuntimeException e) {
             throw (ParseException) new ParseException(e.getLocalizedMessage(), getErrorIndex(e, null)).initCause(e);
         }
     }
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/TemporalDate.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/TemporalDate.java
new file mode 100644
index 0000000..ed081ca
--- /dev/null
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/TemporalDate.java
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.util.privy;
+
+import java.util.Date;
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.LocalTime;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.temporal.Temporal;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+import java.time.chrono.ChronoZonedDateTime;
+import org.apache.sis.util.resources.Errors;
+
+
+/**
+ * A date which is wrapping a {@code java.time} temporal object.
+ * This is used for interoperability in a situation where we are mixing
+ * legacy API working on {@link Date} with newer API working on {@link Temporal}.
+ *
+ * <h2>Design note</h2>
+ * This class intentionally don't implement {@link Temporal} in order to force unwrapping
+ * if a temporal object is desired.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class TemporalDate extends Date {     // Intentionally do not implement Temporal.
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 8239300258490556354L;
+
+    /**
+     * The temporal object wrapped by this date.
+     */
+    @SuppressWarnings("serial")     // Most implementations are serializable.
+    private final Temporal temporal;
+
+    /**
+     * Creates a new date for the given instant.
+     *
+     * @param  temporal the temporal object to wrap in a new date.
+     * @throws ArithmeticException if numeric overflow occurs.
+     */
+    private TemporalDate(final Instant temporal) {
+        super(temporal.toEpochMilli());
+        this.temporal = temporal;
+    }
+
+    /**
+     * Creates a new date for the given temporal.
+     *
+     * @param  temporal the temporal object to wrap in a new date.
+     * @throws ArithmeticException if numeric overflow occurs.
+     */
+    private TemporalDate(final Temporal temporal) {
+        super(toInstant(temporal, ZoneOffset.UTC).toEpochMilli());
+        this.temporal = temporal;
+    }
+
+    /**
+     * Returns the given temporal object as a date.
+     * Used for interoperability in situations where old and new Java API are mixed.
+     *
+     * @param  time  the temporal object to return as a date, or {@code null}.
+     * @return the given temporal object as a date, or {@code null} if the given argument was null.
+     * @throws ArithmeticException if numeric overflow occurs.
+     */
+    public static Date toDate(final Temporal time) {
+        return (time == null) ? null : new TemporalDate(time);
+    }
+
+    /**
+     * Returns the given temporal object as a date.
+     * Used for interoperability in situations where old and new Java API are mixed.
+     *
+     * @param  time  the temporal object to return as a date, or {@code null}.
+     * @return the given temporal object as a date, or {@code null} if the given argument was null.
+     * @throws ArithmeticException if numeric overflow occurs.
+     */
+    public static Date toDate(final Instant time) {
+        return (time == null) ? null : new TemporalDate(time);
+    }
+
+    /**
+     * Returns the given date as a temporal object.
+     * Used for interoperability in situations where old and new Java API are mixed.
+     *
+     * @param  time  the date to return as a temporal object, or {@code null}.
+     * @return the given date as a temporal object, or {@code null} if the given argument was null.
+     */
+    public static Temporal toTemporal(final Date time) {
+        return (time == null) ? null : (time instanceof TemporalDate) ? ((TemporalDate) time).temporal : time.toInstant();
+    }
+
+    /**
+     * Returns the given date as an instant object.
+     * Used for interoperability in situations where old and new Java API are mixed.
+     *
+     * @param  time  the date to return as an instant object, or {@code null}.
+     * @return the given date as an instant object, or {@code null} if the given argument was null.
+     */
+    public static Instant toInstant(final Date time) {
+        return (time == null) ? null : time.toInstant();
+    }
+
+    /**
+     * Converts the given temporal object into an instant.
+     * If the timezone is unspecified, then UTC is assumed.
+     *
+     * @param  date  the temporal object to convert, or {@code null}.
+     * @param  zone  the timezone to use if the time is local, or {@code null} if none.
+     * @return the instant for the given temporal object, or {@code null} if the argument was null.
+     * @throws DateTimeException if the given date does not support a field required by this method.
+     */
+    public static Instant toInstant(final TemporalAccessor date, final ZoneId zone) {
+        if (date == null) {
+            return null;
+        }
+        if (date instanceof Instant) {
+            return (Instant) date;
+        } else if (date instanceof OffsetDateTime) {
+            return ((OffsetDateTime) date).toInstant();
+        } else if (date instanceof ChronoZonedDateTime) {
+            return ((ChronoZonedDateTime) date).toInstant();
+        } else if (zone != null) {
+            if (date instanceof LocalDateTime) {
+                final var t = (LocalDateTime) date;
+                if (zone instanceof ZoneOffset) {
+                    return t.atOffset((ZoneOffset) zone).toInstant();
+                } else {
+                    return t.atZone(zone).toInstant();
+                }
+            } else if (date instanceof LocalDate) {
+                final var t = (LocalDate) date;
+                return t.atStartOfDay(zone).toInstant();
+            }
+        }
+        Instant time;
+        final ChronoField nano;
+        if (zone == null || date.isSupported(ChronoField.INSTANT_SECONDS)) {
+            time = Instant.ofEpochSecond(date.getLong(ChronoField.INSTANT_SECONDS));
+            nano = ChronoField.NANO_OF_SECOND;
+        } else if (zone.equals(ZoneOffset.UTC)) {
+            // Note that the timezone of the temporal value is unknown here. We assume UTC.
+            time = Instant.ofEpochSecond(Math.multiplyExact(date.getLong(ChronoField.EPOCH_DAY), Constants.SECONDS_PER_DAY));
+            nano = ChronoField.NANO_OF_DAY;
+        } else {
+            throw new DateTimeException(Errors.format(Errors.Keys.CanNotConvertFromType_2, date.getClass(), Instant.class));
+        }
+        if (date.isSupported(nano)) {
+            time = time.plusNanos(date.getLong(nano));
+        }
+        return time;
+    }
+
+    /**
+     * Returns this date as an instant.
+     */
+    @Override
+    public Instant toInstant() {
+        if (temporal instanceof Instant) {
+            return (Instant) temporal;
+        }
+        return super.toInstant();
+    }
+
+    /**
+     * Adds the given amount of seconds to the given instant.
+     *
+     * @param  time   the instant to which to add seconds, or {@code null}.
+     * @param  value  number of seconds.
+     * @return the shifted time, or {@code null} if the given instant was null or the given value was NaN.
+     */
+    public static Instant addSeconds(final Instant time, final double value) {
+        if (time == null || Double.isNaN(value)) {
+            return null;
+        }
+        final long r = Math.round(value);
+        return time.plusSeconds(r).plusNanos(Math.round((value - r) * Constants.NANOS_PER_SECOND));
+    }
+
+    /**
+     * Returns {@code true} if objects of the given class have day, month and hour fields.
+     * This method is defined here for having a single class where to concentrate such heuristic rules.
+     * Note that {@link Instant} does not have date fields.
+     *
+     * @param  date  class of object to test (may be {@code null}).
+     * @return whether the given class is {@link LocalDate} or one of the classes with date + time.
+     *         This list may be expanded in future versions.
+     */
+    public static boolean hasDateFields(final Class<?> date) {
+        return date == LocalDate.class
+            || date == LocalDateTime.class
+            || date == OffsetDateTime.class
+            || date == ZonedDateTime.class;
+    }
+
+    /**
+     * Returns {@code true} if objects of the given class have time fields.
+     * This method is defined here for having a single class where to concentrate such heuristic rules.
+     * Note that {@link Instant} does not have hour fields.
+     *
+     * @param  date  class of object to test (may be {@code null}).
+     * @return whether the given class is {@link LocalTime}, {@link OffsetTime} or one of the classes with date + time.
+     *         This list may be expanded in future versions.
+     */
+    public static boolean hasTimeFields(final Class<?> date) {
+        return date == LocalTime.class
+            || date == OffsetTime.class
+            || date == LocalDateTime.class
+            || date == OffsetDateTime.class
+            || date == ZonedDateTime.class;
+    }
+}
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
index 74998ec..77f1059 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
@@ -60,1032 +60,1032 @@
         /**
          * ‘{0}’ is already initialized.
          */
-        public static final short AlreadyInitialized_1 = 188;
+        public static final short AlreadyInitialized_1 = 1;
 
         /**
          * Name “{2}” is ambiguous because it can be understood as either “{0}” or “{1}”.
          */
-        public static final short AmbiguousName_3 = 1;
+        public static final short AmbiguousName_3 = 2;
 
         /**
          * Computation in background failed.
          */
-        public static final short BackgroundComputationFailed = 191;
+        public static final short BackgroundComputationFailed = 3;
 
         /**
          * This object can iterate only once.
          */
-        public static final short CanIterateOnlyOnce = 172;
+        public static final short CanIterateOnlyOnce = 4;
 
         /**
          * No element can be added to this set because properties ‘{0}’ and ‘{1}’ are mutually
          * exclusive.
          */
-        public static final short CanNotAddToExclusiveSet_2 = 2;
+        public static final short CanNotAddToExclusiveSet_2 = 5;
 
         /**
          * Cannot assign units “{1}” to dimension “{0}”.
          */
-        public static final short CanNotAssignUnitToDimension_2 = 3;
+        public static final short CanNotAssignUnitToDimension_2 = 6;
 
         /**
          * Cannot assign units “{1}” to variable “{0}”.
          */
-        public static final short CanNotAssignUnitToVariable_2 = 183;
+        public static final short CanNotAssignUnitToVariable_2 = 7;
 
         /**
          * Cannot assign “{1}” to “{0}”.
          */
-        public static final short CanNotAssign_2 = 4;
+        public static final short CanNotAssign_2 = 8;
 
         /**
          * Cannot compute “{0}”.
          */
-        public static final short CanNotCompute_1 = 5;
+        public static final short CanNotCompute_1 = 9;
 
         /**
          * Cannot connect to “{0}”.
          */
-        public static final short CanNotConnectTo_1 = 6;
+        public static final short CanNotConnectTo_1 = 10;
 
         /**
          * Cannot convert from type ‘{0}’ to type ‘{1}’.
          */
-        public static final short CanNotConvertFromType_2 = 7;
+        public static final short CanNotConvertFromType_2 = 11;
 
         /**
          * Cannot convert value “{0}” to type ‘{1}’.
          */
-        public static final short CanNotConvertValue_2 = 8;
+        public static final short CanNotConvertValue_2 = 12;
 
         /**
          * Cannot copy “{0}”.
          */
-        public static final short CanNotCopy_1 = 169;
+        public static final short CanNotCopy_1 = 13;
 
         /**
          * Cannot open “{0}”.
          */
-        public static final short CanNotOpen_1 = 9;
+        public static final short CanNotOpen_1 = 14;
 
         /**
          * Cannot parse “{0}”.
          */
-        public static final short CanNotParse_1 = 180;
+        public static final short CanNotParse_1 = 15;
 
         /**
          * Cannot process property “{1}” located at path “{0}”. The reason is: {2}
          */
-        public static final short CanNotProcessPropertyAtPath_3 = 184;
+        public static final short CanNotProcessPropertyAtPath_3 = 16;
 
         /**
          * Cannot process property “{0}”. The reason is: {1}
          */
-        public static final short CanNotProcessProperty_2 = 152;
+        public static final short CanNotProcessProperty_2 = 17;
 
         /**
          * Cannot read property “{1}” in file “{0}”.
          */
-        public static final short CanNotReadPropertyInFile_2 = 11;
+        public static final short CanNotReadPropertyInFile_2 = 18;
 
         /**
          * Cannot read “{0}”.
          */
-        public static final short CanNotRead_1 = 12;
+        public static final short CanNotRead_1 = 19;
 
         /**
          * Cannot read “{0}” at line {1}, column {2}.
          */
-        public static final short CanNotRead_3 = 34;
+        public static final short CanNotRead_3 = 20;
 
         /**
          * Cannot represent “{1}” in a strictly standard-compliant {0} format.
          */
-        public static final short CanNotRepresentInFormat_2 = 13;
+        public static final short CanNotRepresentInFormat_2 = 21;
 
         /**
          * Cannot resolve “{0}” as an absolute path.
          */
-        public static final short CanNotResolveAsAbsolutePath_1 = 106;
+        public static final short CanNotResolveAsAbsolutePath_1 = 22;
 
         /**
          * Cannot set a value for parameter “{0}”.
          */
-        public static final short CanNotSetParameterValue_1 = 14;
+        public static final short CanNotSetParameterValue_1 = 23;
 
         /**
          * Cannot set a value for property “{0}”.
          */
-        public static final short CanNotSetPropertyValue_1 = 15;
+        public static final short CanNotSetPropertyValue_1 = 24;
 
         /**
          * Cannot store the {0} value in this vector.
          */
-        public static final short CanNotStoreInVector_1 = 175;
+        public static final short CanNotStoreInVector_1 = 25;
 
         /**
          * Cannot transform envelope.
          */
-        public static final short CanNotTransformEnvelope = 16;
+        public static final short CanNotTransformEnvelope = 26;
 
         /**
          * Cannot write “{1}” as a file in the {0} format.
          */
-        public static final short CanNotWriteFile_2 = 17;
+        public static final short CanNotWriteFile_2 = 27;
 
         /**
          * Circular reference.
          */
-        public static final short CircularReference = 18;
+        public static final short CircularReference = 28;
 
         /**
          * Class ‘{0}’ is not final.
          */
-        public static final short ClassNotFinal_1 = 19;
+        public static final short ClassNotFinal_1 = 29;
 
         /**
          * Cannot clone an object of type ‘{0}’.
          */
-        public static final short CloneNotSupported_1 = 20;
+        public static final short CloneNotSupported_1 = 30;
 
         /**
          * Connection is closed.
          */
-        public static final short ConnectionClosed = 194;
+        public static final short ConnectionClosed = 31;
 
         /**
          * Cross references are not supported.
          */
-        public static final short CrossReferencesNotSupported = 167;
+        public static final short CrossReferencesNotSupported = 32;
 
         /**
          * Database error while creating a ‘{0}’ object for the “{1}” identifier.
          */
-        public static final short DatabaseError_2 = 21;
+        public static final short DatabaseError_2 = 33;
 
         /**
          * Failed to {0,choice,0#insert|1#update} record “{2}” in database table “{1}”.
          */
-        public static final short DatabaseUpdateFailure_3 = 174;
+        public static final short DatabaseUpdateFailure_3 = 34;
 
         /**
          * Thread “{0}” is dead.
          */
-        public static final short DeadThread_1 = 22;
+        public static final short DeadThread_1 = 35;
 
         /**
          * This instance of ‘{0}’ has been disposed.
          */
-        public static final short DisposedInstanceOf_1 = 23;
+        public static final short DisposedInstanceOf_1 = 36;
 
         /**
          * Element “{0}” is duplicated.
          */
-        public static final short DuplicatedElement_1 = 24;
+        public static final short DuplicatedElement_1 = 37;
 
         /**
          * File “{0}” is referenced more than once.
          */
-        public static final short DuplicatedFileReference_1 = 186;
+        public static final short DuplicatedFileReference_1 = 38;
 
         /**
          * Name or identifier “{0}” is used more than once.
          */
-        public static final short DuplicatedIdentifier_1 = 25;
+        public static final short DuplicatedIdentifier_1 = 39;
 
         /**
          * Value {0,number} is used more than once.
          */
-        public static final short DuplicatedNumber_1 = 187;
+        public static final short DuplicatedNumber_1 = 40;
 
         /**
          * Option “{0}” is duplicated.
          */
-        public static final short DuplicatedOption_1 = 26;
+        public static final short DuplicatedOption_1 = 41;
 
         /**
          * Element “{0}” is already present.
          */
-        public static final short ElementAlreadyPresent_1 = 27;
+        public static final short ElementAlreadyPresent_1 = 42;
 
         /**
          * Element “{0}” has not been found.
          */
-        public static final short ElementNotFound_1 = 28;
+        public static final short ElementNotFound_1 = 43;
 
         /**
          * Argument ‘{0}’ shall not be empty.
          */
-        public static final short EmptyArgument_1 = 29;
+        public static final short EmptyArgument_1 = 44;
 
         /**
          * The dictionary shall contain at least one entry.
          */
-        public static final short EmptyDictionary = 30;
+        public static final short EmptyDictionary = 45;
 
         /**
          * Envelope must be at least two-dimensional and non-empty.
          */
-        public static final short EmptyEnvelope2D = 31;
+        public static final short EmptyEnvelope2D = 46;
 
         /**
          * Property named “{0}” shall not be empty.
          */
-        public static final short EmptyProperty_1 = 32;
+        public static final short EmptyProperty_1 = 47;
 
         /**
          * An error occurred in file “{0}” at line {1}.
          */
-        public static final short ErrorInFileAtLine_2 = 33;
+        public static final short ErrorInFileAtLine_2 = 48;
 
         /**
          * A size of {1} elements is excessive for the “{0}” list.
          */
-        public static final short ExcessiveListSize_2 = 36;
+        public static final short ExcessiveListSize_2 = 49;
 
         /**
          * For this algorithm, {0} is an excessive number of dimensions.
          */
-        public static final short ExcessiveNumberOfDimensions_1 = 37;
+        public static final short ExcessiveNumberOfDimensions_1 = 50;
 
         /**
          * No factory of kind ‘{0}’ found.
          */
-        public static final short FactoryNotFound_1 = 38;
+        public static final short FactoryNotFound_1 = 51;
 
         /**
          * File “{0}” has not been found.
          */
-        public static final short FileNotFound_1 = 39;
+        public static final short FileNotFound_1 = 52;
 
         /**
          * Attribute “{0}” is not allowed for an object of type ‘{1}’.
          */
-        public static final short ForbiddenAttribute_2 = 40;
+        public static final short ForbiddenAttribute_2 = 53;
 
         /**
          * Property “{0}” is not allowed.
          */
-        public static final short ForbiddenProperty_1 = 41;
+        public static final short ForbiddenProperty_1 = 54;
 
         /**
          * “{0}” uses two or more different units of measurement.
          */
-        public static final short HeterogynousUnitsIn_1 = 202;
+        public static final short HeterogynousUnitsIn_1 = 55;
 
         /**
          * Identifier “{1}” is not in “{0}” namespace.
          */
-        public static final short IdentifierNotInNamespace_2 = 199;
+        public static final short IdentifierNotInNamespace_2 = 56;
 
         /**
          * Argument ‘{0}’ cannot be an instance of ‘{1}’.
          */
-        public static final short IllegalArgumentClass_2 = 42;
+        public static final short IllegalArgumentClass_2 = 57;
 
         /**
          * Argument ‘{0}’ cannot be an instance of ‘{2}’. Expected an instance of ‘{1}’ or derived
          * type.
          */
-        public static final short IllegalArgumentClass_3 = 43;
+        public static final short IllegalArgumentClass_3 = 58;
 
         /**
          * Argument ‘{0}’ cannot take the “{1}” value.
          */
-        public static final short IllegalArgumentValue_2 = 45;
+        public static final short IllegalArgumentValue_2 = 59;
 
         /**
          * Illegal bits pattern: {0}.
          */
-        public static final short IllegalBitsPattern_1 = 46;
+        public static final short IllegalBitsPattern_1 = 60;
 
         /**
          * Coordinate reference system cannot be of type ‘{0}’.
          */
-        public static final short IllegalCRSType_1 = 47;
+        public static final short IllegalCRSType_1 = 61;
 
         /**
          * The “{2}” character in “{1}” is not permitted by the “{0}” format.
          */
-        public static final short IllegalCharacterForFormat_3 = 48;
+        public static final short IllegalCharacterForFormat_3 = 62;
 
         /**
          * The “{1}” character cannot be used for “{0}”.
          */
-        public static final short IllegalCharacter_2 = 49;
+        public static final short IllegalCharacter_2 = 63;
 
         /**
          * Class ‘{1}’ is illegal. It must be ‘{0}’ or a derived class.
          */
-        public static final short IllegalClass_2 = 50;
+        public static final short IllegalClass_2 = 64;
 
         /**
          * The [{0} … {1}] range of coordinate values is not valid for the “{2}” axis.
          */
-        public static final short IllegalCoordinateRange_3 = 57;
+        public static final short IllegalCoordinateRange_3 = 65;
 
         /**
          * Coordinate system cannot be “{0}”.
          */
-        public static final short IllegalCoordinateSystem_1 = 51;
+        public static final short IllegalCoordinateSystem_1 = 66;
 
         /**
          * The “{1}” pattern cannot be applied to formatting of objects of type ‘{0}’.
          */
-        public static final short IllegalFormatPatternForClass_2 = 52;
+        public static final short IllegalFormatPatternForClass_2 = 67;
 
         /**
          * “{1}” is not a valid identifier for the “{0}” code space.
          */
-        public static final short IllegalIdentifierForCodespace_2 = 53;
+        public static final short IllegalIdentifierForCodespace_2 = 68;
 
         /**
          * The “{0}” language is not recognized.
          */
-        public static final short IllegalLanguageCode_1 = 54;
+        public static final short IllegalLanguageCode_1 = 69;
 
         /**
          * Illegal mapping: {0} → {1}.
          */
-        public static final short IllegalMapping_2 = 185;
+        public static final short IllegalMapping_2 = 70;
 
         /**
          * Member “{0}” cannot be associated to type “{1}”.
          */
-        public static final short IllegalMemberType_2 = 55;
+        public static final short IllegalMemberType_2 = 71;
 
         /**
          * Option ‘{0}’ cannot take the “{1}” value.
          */
-        public static final short IllegalOptionValue_2 = 56;
+        public static final short IllegalOptionValue_2 = 72;
 
         /**
          * Property “{0}” does not accept instances of ‘{1}’.
          */
-        public static final short IllegalPropertyValueClass_2 = 58;
+        public static final short IllegalPropertyValueClass_2 = 73;
 
         /**
          * Expected an instance of ‘{1}’ for the “{0}” property, but got an instance of ‘{2}’.
          */
-        public static final short IllegalPropertyValueClass_3 = 59;
+        public static final short IllegalPropertyValueClass_3 = 74;
 
         /**
          * Property “{0}” cannot take the “{1}” value.
          */
-        public static final short IllegalPropertyValue_2 = 198;
+        public static final short IllegalPropertyValue_2 = 75;
 
         /**
          * Range [{0} … {1}] is not valid.
          */
-        public static final short IllegalRange_2 = 60;
+        public static final short IllegalRange_2 = 76;
 
         /**
          * Sexagesimal angle {0,number} is illegal because the {1,choice,0#minutes|1#seconds} field
          * cannot take the {2,number} value.
          */
-        public static final short IllegalSexagesimalField_3 = 44;
+        public static final short IllegalSexagesimalField_3 = 77;
 
         /**
          * Value {1} for “{0}” is not a valid Unicode code point.
          */
-        public static final short IllegalUnicodeCodePoint_2 = 61;
+        public static final short IllegalUnicodeCodePoint_2 = 78;
 
         /**
          * Illegal value for property “{1}” in “{0}”.
          */
-        public static final short IllegalValueForProperty_2 = 196;
+        public static final short IllegalValueForProperty_2 = 79;
 
         /**
          * Cannot use the {1} format with “{0}”.
          */
-        public static final short IncompatibleFormat_2 = 62;
+        public static final short IncompatibleFormat_2 = 80;
 
         /**
          * Property “{0}” has an incompatible value.
          */
-        public static final short IncompatiblePropertyValue_1 = 63;
+        public static final short IncompatiblePropertyValue_1 = 81;
 
         /**
          * The “{0}” unit of measurement has dimension of ‘{1}’ ({2}). It is incompatible with
          * dimension of ‘{3}’ ({4}).
          */
-        public static final short IncompatibleUnitDimension_5 = 64;
+        public static final short IncompatibleUnitDimension_5 = 82;
 
         /**
          * Unit “{0}” is incompatible with current value.
          */
-        public static final short IncompatibleUnit_1 = 65;
+        public static final short IncompatibleUnit_1 = 83;
 
         /**
          * Units “{0}” and “{1}” are incompatible.
          */
-        public static final short IncompatibleUnits_2 = 66;
+        public static final short IncompatibleUnits_2 = 84;
 
         /**
          * Value “{1}” of attribute ‘{0}’ is inconsistent with other attributes.
          */
-        public static final short InconsistentAttribute_2 = 67;
+        public static final short InconsistentAttribute_2 = 85;
 
         /**
          * Inconsistent table columns.
          */
-        public static final short InconsistentTableColumns = 69;
+        public static final short InconsistentTableColumns = 86;
 
         /**
          * Unit of measurement “{0}” is inconsistent with coordinate system axes.
          */
-        public static final short InconsistentUnitsForCS_1 = 70;
+        public static final short InconsistentUnitsForCS_1 = 87;
 
         /**
          * Index {0} is out of bounds.
          */
-        public static final short IndexOutOfBounds_1 = 71;
+        public static final short IndexOutOfBounds_1 = 88;
 
         /**
          * Indices ({0}, {1}) are out of bounds.
          */
-        public static final short IndicesOutOfBounds_2 = 72;
+        public static final short IndicesOutOfBounds_2 = 89;
 
         /**
          * Argument ‘{0}’ cannot take an infinite value.
          */
-        public static final short InfiniteArgumentValue_1 = 73;
+        public static final short InfiniteArgumentValue_1 = 90;
 
         /**
          * Integer overflow during {0} bits arithmetic operation.
          */
-        public static final short IntegerOverflow_1 = 10;
+        public static final short IntegerOverflow_1 = 91;
 
         /**
          * Interrupted while waiting result.
          */
-        public static final short InterruptedWhileWaitingResult = 192;
+        public static final short InterruptedWhileWaitingResult = 92;
 
         /**
          * “{0}” is an invalid version identifier.
          */
-        public static final short InvalidVersionIdentifier_1 = 179;
+        public static final short InvalidVersionIdentifier_1 = 93;
 
         /**
          * Key “{0}” is associated twice to different values.
          */
-        public static final short KeyCollision_1 = 75;
+        public static final short KeyCollision_1 = 94;
 
         /**
          * Attribute “{0}” is mandatory for an object of type ‘{1}’.
          */
-        public static final short MandatoryAttribute_2 = 76;
+        public static final short MandatoryAttribute_2 = 95;
 
         /**
          * Mismatched array lengths.
          */
-        public static final short MismatchedArrayLengths = 77;
+        public static final short MismatchedArrayLengths = 96;
 
         /**
          * Mismatched axes “{1}” and “{2}” at dimension {0}.
          */
-        public static final short MismatchedAxes_3 = 200;
+        public static final short MismatchedAxes_3 = 97;
 
         /**
          * The coordinate reference system must be the same for all objects.
          */
-        public static final short MismatchedCRS = 78;
+        public static final short MismatchedCRS = 98;
 
         /**
          * The “{0}” coordinate reference system has {1} dimension{1,choice,1#|2#s}, but the given
          * geometry is {2}-dimensional.
          */
-        public static final short MismatchedDimensionForCRS_3 = 79;
+        public static final short MismatchedDimensionForCRS_3 = 99;
 
         /**
          * Mismatched object dimensions: {0}D and {1}D.
          */
-        public static final short MismatchedDimension_2 = 80;
+        public static final short MismatchedDimension_2 = 100;
 
         /**
          * Argument ‘{0}’ has {2} dimension{2,choice,1#|2#s}, while {1} was expected.
          */
-        public static final short MismatchedDimension_3 = 81;
+        public static final short MismatchedDimension_3 = 101;
 
         /**
          * The grid geometry must be the same for “{0}” and “{1}”.
          */
-        public static final short MismatchedGridGeometry_2 = 82;
+        public static final short MismatchedGridGeometry_2 = 102;
 
         /**
          * Mismatched matrix sizes: expected {0}×{1} but got {2}×{3}.
          */
-        public static final short MismatchedMatrixSize_4 = 83;
+        public static final short MismatchedMatrixSize_4 = 103;
 
         /**
          * The “{0}” transform has {3} {1,choice,0#source|1#target} dimension{3,choice,1#|2#s}, while
          * {2} was expected.
          */
-        public static final short MismatchedTransformDimension_4 = 190;
+        public static final short MismatchedTransformDimension_4 = 104;
 
         /**
          * Missing a ‘{1}’ character in “{0}” element.
          */
-        public static final short MissingCharacterInElement_2 = 84;
+        public static final short MissingCharacterInElement_2 = 105;
 
         /**
          * Missing a “{1}” component in “{0}”.
          */
-        public static final short MissingComponentInElement_2 = 85;
+        public static final short MissingComponentInElement_2 = 106;
 
         /**
          * JAXB context has not been specified.
          */
-        public static final short MissingJAXBContext = 86;
+        public static final short MissingJAXBContext = 107;
 
         /**
          * Missing or empty ‘{1}’ attribute in “{0}”.
          */
-        public static final short MissingOrEmptyAttribute_2 = 182;
+        public static final short MissingOrEmptyAttribute_2 = 108;
 
         /**
          * This operation requires the “{0}” module.
          */
-        public static final short MissingRequiredModule_1 = 87;
+        public static final short MissingRequiredModule_1 = 109;
 
         /**
          * Missing value for “{0}” option.
          */
-        public static final short MissingValueForOption_1 = 88;
+        public static final short MissingValueForOption_1 = 110;
 
         /**
          * Missing value for “{0}” property.
          */
-        public static final short MissingValueForProperty_1 = 89;
+        public static final short MissingValueForProperty_1 = 111;
 
         /**
          * Missing value for “{1}” property in “{0}”.
          */
-        public static final short MissingValueForProperty_2 = 197;
+        public static final short MissingValueForProperty_2 = 112;
 
         /**
          * Missing value in the “{0}” column.
          */
-        public static final short MissingValueInColumn_1 = 90;
+        public static final short MissingValueInColumn_1 = 113;
 
         /**
          * Cannot return a single value for “{0}” because there is at least two occurrences, at indices
          * {1} and {2}.
          */
-        public static final short MultiOccurenceValueAtIndices_3 = 173;
+        public static final short MultiOccurenceValueAtIndices_3 = 114;
 
         /**
          * Options “{0}” and “{1}” are mutually exclusive.
          */
-        public static final short MutuallyExclusiveOptions_2 = 91;
+        public static final short MutuallyExclusiveOptions_2 = 115;
 
         /**
          * Native interfaces “{1}” not available for the {0} platform.
          */
-        public static final short NativeInterfacesNotFound_2 = 176;
+        public static final short NativeInterfacesNotFound_2 = 116;
 
         /**
          * Argument ‘{0}’ shall not be negative. The given value was {1}.
          */
-        public static final short NegativeArgument_2 = 92;
+        public static final short NegativeArgument_2 = 117;
 
         /**
          * Cannot create a “{0}” array of negative length.
          */
-        public static final short NegativeArrayLength_1 = 93;
+        public static final short NegativeArrayLength_1 = 118;
 
         /**
          * Nested “{0}” elements are not allowed.
          */
-        public static final short NestedElementNotAllowed_1 = 94;
+        public static final short NestedElementNotAllowed_1 = 119;
 
         /**
          * The object is nil for the following reason: {0}.
          */
-        public static final short NilObject_1 = 204;
+        public static final short NilObject_1 = 120;
 
         /**
          * No value is associated to “{0}”.
          */
-        public static final short NoSuchValue_1 = 95;
+        public static final short NoSuchValue_1 = 121;
 
         /**
          * Node “{0}” cannot be a child of itself.
          */
-        public static final short NodeChildOfItself_1 = 96;
+        public static final short NodeChildOfItself_1 = 122;
 
         /**
          * Node “{0}” already has another parent.
          */
-        public static final short NodeHasAnotherParent_1 = 97;
+        public static final short NodeHasAnotherParent_1 = 123;
 
         /**
          * Node “{0}” has no parent.
          */
-        public static final short NodeHasNoParent_1 = 98;
+        public static final short NodeHasNoParent_1 = 124;
 
         /**
          * Node “{0}” is a leaf.
          */
-        public static final short NodeIsLeaf_1 = 99;
+        public static final short NodeIsLeaf_1 = 125;
 
         /**
          * “{0}” is not an angular unit.
          */
-        public static final short NonAngularUnit_1 = 100;
+        public static final short NonAngularUnit_1 = 126;
 
         /**
          * Missing a ‘{1}’ parenthesis in “{0}”.
          */
-        public static final short NonEquilibratedParenthesis_2 = 101;
+        public static final short NonEquilibratedParenthesis_2 = 127;
 
         /**
          * No horizontal component found in the “{0}” coordinate reference system.
          */
-        public static final short NonHorizontalCRS_1 = 201;
+        public static final short NonHorizontalCRS_1 = 128;
 
         /**
          * Conversion is not invertible.
          */
-        public static final short NonInvertibleConversion = 102;
+        public static final short NonInvertibleConversion = 129;
 
         /**
          * “{0}” is not a linear unit.
          */
-        public static final short NonLinearUnit_1 = 103;
+        public static final short NonLinearUnit_1 = 130;
 
         /**
          * The scale of measurement for “{0}” unit is not a ratio scale.
          */
-        public static final short NonRatioUnit_1 = 104;
+        public static final short NonRatioUnit_1 = 131;
 
         /**
          * “{0}” is not a scale unit.
          */
-        public static final short NonScaleUnit_1 = 105;
+        public static final short NonScaleUnit_1 = 132;
 
         /**
          * “{0}” is not a time unit.
          */
-        public static final short NonTemporalUnit_1 = 107;
+        public static final short NonTemporalUnit_1 = 133;
 
         /**
          * No element for the “{0}” identifier, or the identifier is a forward reference.
          */
-        public static final short NotABackwardReference_1 = 108;
+        public static final short NotABackwardReference_1 = 134;
 
         /**
          * Value of ‘{0}’ shall be a {1,choice,0#divisor|1#multiple} of {2} but the given value is {3}.
          */
-        public static final short NotADivisorOrMultiple_4 = 193;
+        public static final short NotADivisorOrMultiple_4 = 135;
 
         /**
          * “{0}” is not a key-value pair.
          */
-        public static final short NotAKeyValuePair_1 = 109;
+        public static final short NotAKeyValuePair_1 = 136;
 
         /**
          * Argument ‘{0}’ shall not be NaN (Not-a-Number).
          */
-        public static final short NotANumber_1 = 110;
+        public static final short NotANumber_1 = 137;
 
         /**
          * Class ‘{0}’ is not a primitive type wrapper.
          */
-        public static final short NotAPrimitiveWrapper_1 = 111;
+        public static final short NotAPrimitiveWrapper_1 = 138;
 
         /**
          * Text “{0}” is not a Unicode identifier.
          */
-        public static final short NotAUnicodeIdentifier_1 = 112;
+        public static final short NotAUnicodeIdentifier_1 = 139;
 
         /**
          * {0} is not an integer value.
          */
-        public static final short NotAnInteger_1 = 171;
+        public static final short NotAnInteger_1 = 140;
 
         /**
          * Argument ‘{0}’ shall not be null.
          */
-        public static final short NullArgument_1 = 113;
+        public static final short NullArgument_1 = 141;
 
         /**
          * ‘{0}’ collection does not accept null elements.
          */
-        public static final short NullCollectionElement_1 = 114;
+        public static final short NullCollectionElement_1 = 142;
 
         /**
          * Null key is not allowed in this dictionary.
          */
-        public static final short NullMapKey = 115;
+        public static final short NullMapKey = 143;
 
         /**
          * Null values are not allowed in this dictionary.
          */
-        public static final short NullMapValue = 116;
+        public static final short NullMapValue = 144;
 
         /**
          * Unexpected null value in record “{2}” for the column “{1}” in table “{0}”.
          */
-        public static final short NullValueInTable_3 = 117;
+        public static final short NullValueInTable_3 = 145;
 
         /**
          * Array length is {0}, while we expected an even length.
          */
-        public static final short OddArrayLength_1 = 118;
+        public static final short OddArrayLength_1 = 146;
 
         /**
          * “{1}” is opened in {0,choice,0#read|1#write}-only mode.
          */
-        public static final short OpenedReadOrWriteOnly_2 = 203;
+        public static final short OpenedReadOrWriteOnly_2 = 147;
 
         /**
          * Coordinate is outside the domain of validity.
          */
-        public static final short OutsideDomainOfValidity = 119;
+        public static final short OutsideDomainOfValidity = 148;
 
         /**
          * No property named “{1}” has been found in “{0}”.
          */
-        public static final short PropertyNotFound_2 = 120;
+        public static final short PropertyNotFound_2 = 149;
 
         /**
          * Record “{1}” is already defined in schema “{0}”.
          */
-        public static final short RecordAlreadyDefined_2 = 121;
+        public static final short RecordAlreadyDefined_2 = 150;
 
         /**
          * No record found in “{0}” table for “{1}” key.
          */
-        public static final short RecordNotFound_2 = 122;
+        public static final short RecordNotFound_2 = 151;
 
         /**
          * Recursive call while creating an object for the “{0}” key.
          */
-        public static final short RecursiveCreateCallForKey_1 = 123;
+        public static final short RecursiveCreateCallForKey_1 = 152;
 
         /**
          * A decimal separator is required.
          */
-        public static final short RequireDecimalSeparator = 124;
+        public static final short RequireDecimalSeparator = 153;
 
         /**
          * Thread “{0}” seems stalled.
          */
-        public static final short StalledThread_1 = 125;
+        public static final short StalledThread_1 = 154;
 
         /**
          * Table “{0}” has not been found.
          */
-        public static final short TableNotFound_1 = 126;
+        public static final short TableNotFound_1 = 155;
 
         /**
          * Expected at least {0,number} argument{0,choice,1#|2#s}, but got {1,number}.
          */
-        public static final short TooFewArguments_2 = 127;
+        public static final short TooFewArguments_2 = 156;
 
         /**
          * Collection “{0}” contains only {2,number} element{2,choice,1#|2#s} while at least {1,number}
          * elements were expected.
          */
-        public static final short TooFewCollectionElements_3 = 74;
+        public static final short TooFewCollectionElements_3 = 157;
 
         /**
          * Too few occurrences of “{1}”. Expected at least {0,number} of them.
          */
-        public static final short TooFewOccurrences_2 = 128;
+        public static final short TooFewOccurrences_2 = 158;
 
         /**
          * Expected at most {0,number} argument{0,choice,1#|2#s}, but got {1,number}.
          */
-        public static final short TooManyArguments_2 = 129;
+        public static final short TooManyArguments_2 = 159;
 
         /**
          * Collection “{0}” contains {2,number} elements while at most {1,number} element{1,choice,1#
          * was|2#s were} expected.
          */
-        public static final short TooManyCollectionElements_3 = 35;
+        public static final short TooManyCollectionElements_3 = 160;
 
         /**
          * Too many occurrences of “{1}”. The maximum is {0,number}.
          */
-        public static final short TooManyOccurrences_2 = 130;
+        public static final short TooManyOccurrences_2 = 161;
 
         /**
          * Tree depth exceeds the maximum.
          */
-        public static final short TreeDepthExceedsMaximum = 131;
+        public static final short TreeDepthExceedsMaximum = 162;
 
         /**
          * Ordering between “{0}” and “{1}” elements is undefined.
          */
-        public static final short UndefinedOrderingForElements_2 = 132;
+        public static final short UndefinedOrderingForElements_2 = 163;
 
         /**
          * Expected an array of length {0,number}, but got {1,number}.
          */
-        public static final short UnexpectedArrayLength_2 = 133;
+        public static final short UnexpectedArrayLength_2 = 164;
 
         /**
          * Unexpected change in ‘{0}’.
          */
-        public static final short UnexpectedChange_1 = 134;
+        public static final short UnexpectedChange_1 = 165;
 
         /**
          * The “{1}” characters after “{0}” were unexpected.
          */
-        public static final short UnexpectedCharactersAfter_2 = 135;
+        public static final short UnexpectedCharactersAfter_2 = 166;
 
         /**
          * Text for ‘{0}’ was expected to {1,choice,0#begin|1#end} with “{2}”, but found “{3}”.
          */
-        public static final short UnexpectedCharactersAtBound_4 = 136;
+        public static final short UnexpectedCharactersAtBound_4 = 167;
 
         /**
          * Unexpected end of file while reading “{0}”.
          */
-        public static final short UnexpectedEndOfFile_1 = 137;
+        public static final short UnexpectedEndOfFile_1 = 168;
 
         /**
          * More characters were expected at the end of “{0}”.
          */
-        public static final short UnexpectedEndOfString_1 = 138;
+        public static final short UnexpectedEndOfString_1 = 169;
 
         /**
          * File “{1}” seems to be encoded in another format than {0}.
          */
-        public static final short UnexpectedFileFormat_2 = 139;
+        public static final short UnexpectedFileFormat_2 = 170;
 
         /**
          * The “{1}” name is not valid in this context, because the “{0}” namespace was expected.
          */
-        public static final short UnexpectedNamespace_2 = 68;
+        public static final short UnexpectedNamespace_2 = 171;
 
         /**
          * Parameter “{0}” was not expected.
          */
-        public static final short UnexpectedParameter_1 = 140;
+        public static final short UnexpectedParameter_1 = 172;
 
         /**
          * Property “{1}” was not expected in “{0}”.
          */
-        public static final short UnexpectedProperty_2 = 141;
+        public static final short UnexpectedProperty_2 = 173;
 
         /**
          * Unexpected scale factor {1,number} for unit of measurement “{0}”.
          */
-        public static final short UnexpectedScaleFactorForUnit_2 = 142;
+        public static final short UnexpectedScaleFactorForUnit_2 = 174;
 
         /**
          * Expected “{0}” to reference an instance of ‘{1}’, but found an instance of ‘{2}’.
          */
-        public static final short UnexpectedTypeForReference_3 = 143;
+        public static final short UnexpectedTypeForReference_3 = 175;
 
         /**
          * Unexpected value “{1}” in “{0}” element.
          */
-        public static final short UnexpectedValueInElement_2 = 144;
+        public static final short UnexpectedValueInElement_2 = 176;
 
         /**
          * ‘{0}’ has not been initialized.
          */
-        public static final short Uninitialized_1 = 189;
+        public static final short Uninitialized_1 = 177;
 
         /**
          * Command “{0}” is not recognized.
          */
-        public static final short UnknownCommand_1 = 145;
+        public static final short UnknownCommand_1 = 178;
 
         /**
          * “{1}” is not a known or supported value for the ‘{0}’ enumeration.
          */
-        public static final short UnknownEnumValue_2 = 146;
+        public static final short UnknownEnumValue_2 = 179;
 
         /**
          * Keyword “{0}” is unknown.
          */
-        public static final short UnknownKeyword_1 = 147;
+        public static final short UnknownKeyword_1 = 180;
 
         /**
          * Option “{0}” is not recognized.
          */
-        public static final short UnknownOption_1 = 148;
+        public static final short UnknownOption_1 = 181;
 
         /**
          * Type ‘{0}’ is unknown in this context.
          */
-        public static final short UnknownType_1 = 149;
+        public static final short UnknownType_1 = 182;
 
         /**
          * Unit “{0}” is not recognized.
          */
-        public static final short UnknownUnit_1 = 150;
+        public static final short UnknownUnit_1 = 183;
 
         /**
          * The cell at column “{1}” of row “{0}” is unmodifiable.
          */
-        public static final short UnmodifiableCellValue_2 = 151;
+        public static final short UnmodifiableCellValue_2 = 184;
 
         /**
          * This instance of ‘{0}’ is not modifiable.
          */
-        public static final short UnmodifiableObject_1 = 153;
+        public static final short UnmodifiableObject_1 = 185;
 
         /**
          * Text “{1}” cannot be parsed as an object of type ‘{0}’.
          */
-        public static final short UnparsableStringForClass_2 = 154;
+        public static final short UnparsableStringForClass_2 = 186;
 
         /**
          * Text “{1}” cannot be parsed as an object of type ‘{0}’, because of the “{2}” characters.
          */
-        public static final short UnparsableStringForClass_3 = 155;
+        public static final short UnparsableStringForClass_3 = 187;
 
         /**
          * Cannot parse “{1}” in element “{0}”.
          */
-        public static final short UnparsableStringInElement_2 = 156;
+        public static final short UnparsableStringInElement_2 = 188;
 
         /**
          * Coordinate reference system has not been specified.
          */
-        public static final short UnspecifiedCRS = 157;
+        public static final short UnspecifiedCRS = 189;
 
         /**
          * No format is specified for objects of class ‘{0}’.
          */
-        public static final short UnspecifiedFormatForClass_1 = 158;
+        public static final short UnspecifiedFormatForClass_1 = 190;
 
         /**
          * The “{0}” argument value is unsupported.
          */
-        public static final short UnsupportedArgumentValue_1 = 170;
+        public static final short UnsupportedArgumentValue_1 = 191;
 
         /**
          * Axes with “{0}” direction are not supported by this operation.
          */
-        public static final short UnsupportedAxisDirection_1 = 177;
+        public static final short UnsupportedAxisDirection_1 = 192;
 
         /**
          * The “{0}” coordinate system is not supported by this operation.
          */
-        public static final short UnsupportedCoordinateSystem_1 = 178;
+        public static final short UnsupportedCoordinateSystem_1 = 193;
 
         /**
          * The “{0}” datum is not supported by this operation.
          */
-        public static final short UnsupportedDatum_1 = 168;
+        public static final short UnsupportedDatum_1 = 194;
 
         /**
          * Version {1} of {0} format is not supported.
          */
-        public static final short UnsupportedFormatVersion_2 = 159;
+        public static final short UnsupportedFormatVersion_2 = 195;
 
         /**
          * Format “{0}” is unsupported.
          */
-        public static final short UnsupportedFormat_1 = 181;
+        public static final short UnsupportedFormat_1 = 196;
 
         /**
          * Cannot handle this instance of ‘{0}’ because arbitrary implementations are not yet
          * supported.
          */
-        public static final short UnsupportedImplementation_1 = 160;
+        public static final short UnsupportedImplementation_1 = 197;
 
         /**
          * The “{0}” interpolation is unsupported.
          */
-        public static final short UnsupportedInterpolation_1 = 161;
+        public static final short UnsupportedInterpolation_1 = 198;
 
         /**
          * The ‘{0}’ operation is unsupported.
          */
-        public static final short UnsupportedOperation_1 = 162;
+        public static final short UnsupportedOperation_1 = 199;
 
         /**
          * The ‘{0}’ type is not supported in this context.
          */
-        public static final short UnsupportedType_1 = 163;
+        public static final short UnsupportedType_1 = 200;
 
         /**
          * XPath “{0}” is not recognized. The current implementation supports only simple paths.
          */
-        public static final short UnsupportedXPath_1 = 195;
+        public static final short UnsupportedXPath_1 = 201;
 
         /**
          * A value is already defined for “{0}”.
          */
-        public static final short ValueAlreadyDefined_1 = 164;
+        public static final short ValueAlreadyDefined_1 = 202;
 
         /**
          * Value ‘{0}’ = {1,number} is invalid. Expected a number greater than 0.
          */
-        public static final short ValueNotGreaterThanZero_2 = 165;
+        public static final short ValueNotGreaterThanZero_2 = 203;
 
         /**
          * Value ‘{0}’ = {3} is invalid. Expected a value in the [{1} … {2}] range.
          */
-        public static final short ValueOutOfRange_4 = 166;
+        public static final short ValueOutOfRange_4 = 204;
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.util/test/org/apache/sis/measure/RangeFormatTest.java b/endorsed/src/org.apache.sis.util/test/org/apache/sis/measure/RangeFormatTest.java
index eeae5b3..7e2772d 100644
--- a/endorsed/src/org.apache.sis.util/test/org/apache/sis/measure/RangeFormatTest.java
+++ b/endorsed/src/org.apache.sis.util/test/org/apache/sis/measure/RangeFormatTest.java
@@ -30,7 +30,7 @@
 import static java.lang.StrictMath.*;
 import static java.lang.Double.POSITIVE_INFINITY;
 import static java.lang.Double.NEGATIVE_INFINITY;
-import static org.apache.sis.util.privy.StandardDateFormat.UTC;
+import static org.apache.sis.util.privy.Constants.UTC;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
diff --git a/endorsed/src/org.apache.sis.util/test/org/apache/sis/measure/UnitFormatTest.java b/endorsed/src/org.apache.sis.util/test/org/apache/sis/measure/UnitFormatTest.java
index acddb61..12b52b6 100644
--- a/endorsed/src/org.apache.sis.util/test/org/apache/sis/measure/UnitFormatTest.java
+++ b/endorsed/src/org.apache.sis.util/test/org/apache/sis/measure/UnitFormatTest.java
@@ -87,6 +87,7 @@
         verify(declared, "ARC_MINUTE",          "",             "′",     "arc-minute",              Units.ARC_MINUTE);
         verify(declared, "ARC_SECOND",          "",             "″",     "arc-second",              Units.ARC_SECOND);
         verify(declared, "GRAD",                "",             "grad",  "grad",                    Units.GRAD);
+        verify(declared, "NANOSECOND",          "T",            "ns",    "nanosecond",              Units.NANOSECOND);
         verify(declared, "MILLISECOND",         "T",            "ms",    "millisecond",             Units.MILLISECOND);
         verify(declared, "SECOND",              "T",            "s",     "second",                  Units.SECOND);
         verify(declared, "MINUTE",              "T",            "min",   "minute",                  Units.MINUTE);
diff --git a/endorsed/src/org.apache.sis.util/test/org/apache/sis/test/TestUtilities.java b/endorsed/src/org.apache.sis.util/test/org/apache/sis/test/TestUtilities.java
index 3221913..ba464b6 100644
--- a/endorsed/src/org.apache.sis.util/test/org/apache/sis/test/TestUtilities.java
+++ b/endorsed/src/org.apache.sis.util/test/org/apache/sis/test/TestUtilities.java
@@ -47,7 +47,7 @@
 import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.util.collection.TreeTableFormat;
 import org.apache.sis.util.privy.X364;
-import static org.apache.sis.util.privy.StandardDateFormat.UTC;
+import static org.apache.sis.util.privy.Constants.UTC;
 
 // Test dependencies
 import static org.junit.jupiter.api.Assertions.*;
@@ -235,19 +235,6 @@
     }
 
     /**
-     * Formats the given date using the {@code "yyyy-MM-dd HH:mm:ss"} pattern in UTC timezone.
-     *
-     * @param  date  the date to format.
-     * @return the date as a {@link String}.
-     */
-    public static String format(final Date date) {
-        ArgumentChecks.ensureNonNull("date", date);
-        synchronized (dateFormat) {
-            return dateFormat.format(date);
-        }
-    }
-
-    /**
      * Formats the given value using the given formatter, and parses the text back to its value.
      * If the parsed value is not equal to the original one, an {@link AssertionError} is thrown.
      *
diff --git a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/collection/CacheTest.java b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/collection/CacheTest.java
index fe06e32..9c92a1e 100644
--- a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/collection/CacheTest.java
+++ b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/collection/CacheTest.java
@@ -29,7 +29,7 @@
 import org.apache.sis.math.Statistics;
 import org.apache.sis.math.StatisticsFormat;
 import org.apache.sis.util.CharSequences;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.Constants;
 
 // Test dependencies
 import org.junit.jupiter.api.Tag;
@@ -290,7 +290,7 @@
         for (int i=0; i<10; i++) {
             final long t = System.nanoTime();
             out.printf("Cache size: %4d (after %3d ms)%n", cache.size(),
-                       round((t - time) / (double) StandardDateFormat.NANOS_PER_MILLISECOND));
+                       round((t - time) / (double) Constants.NANOS_PER_MILLISECOND));
             time = t;
             Thread.sleep(250);
             if (i >= 2) {
diff --git a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/collection/RangeSetTest.java b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/collection/RangeSetTest.java
index 2e216b8..685210c 100644
--- a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/collection/RangeSetTest.java
+++ b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/collection/RangeSetTest.java
@@ -25,8 +25,8 @@
 import java.util.SortedSet;
 import org.apache.sis.measure.Range;
 import org.apache.sis.measure.NumberRange;
-import static org.apache.sis.util.privy.StandardDateFormat.MILLISECONDS_PER_DAY;
-import static org.apache.sis.util.privy.StandardDateFormat.NANOS_PER_SECOND;
+import static org.apache.sis.util.privy.Constants.MILLISECONDS_PER_DAY;
+import static org.apache.sis.util.privy.Constants.NANOS_PER_SECOND;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
diff --git a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/privy/ConstantsTest.java b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/privy/ConstantsTest.java
new file mode 100644
index 0000000..aab12ff
--- /dev/null
+++ b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/privy/ConstantsTest.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.util.privy;
+
+import java.util.concurrent.TimeUnit;
+
+// Test dependencies
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import org.apache.sis.test.TestCase;
+
+
+/**
+ * Tests the {@link ConstantsTest} class.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class ConstantsTest extends TestCase {
+    /**
+     * Creates a new test case.
+     */
+    public ConstantsTest() {
+    }
+
+    /**
+     * Verifies the constant values related to time duration.
+     */
+    @Test
+    public void verifyConstantValues() {
+        assertEquals(TimeUnit.DAYS.toSeconds(1),       Constants.SECONDS_PER_DAY);
+        assertEquals(TimeUnit.DAYS.toMillis(1),        Constants.MILLISECONDS_PER_DAY);
+        assertEquals(TimeUnit.DAYS.toNanos(1),         Constants.NANOSECONDS_PER_DAY);
+        assertEquals(TimeUnit.MILLISECONDS.toNanos(1), Constants.NANOS_PER_MILLISECOND);
+        assertEquals(TimeUnit.SECONDS.toNanos(1),      Constants.NANOS_PER_SECOND);
+        assertEquals(365.24219 * (24*60*60 * 1000),    Constants.MILLIS_PER_TROPICAL_YEAR, 0.00001 * (24*60*60 * 1000));
+    }
+}
diff --git a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/privy/StandardDateFormatTest.java b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/privy/StandardDateFormatTest.java
index dbe092e..d20a8c7 100644
--- a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/privy/StandardDateFormatTest.java
+++ b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/privy/StandardDateFormatTest.java
@@ -20,7 +20,6 @@
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.util.Date;
-import java.util.concurrent.TimeUnit;
 import java.text.ParseException;
 
 // Test dependencies
@@ -43,17 +42,6 @@
     }
 
     /**
-     * Verifies the {@link StandardDateFormat#MILLISECONDS_PER_DAY}, {@link StandardDateFormat#NANOS_PER_MILLISECOND}
-     * and {@link StandardDateFormat#NANOS_PER_SECOND} constant values.
-     */
-    @Test
-    public void verifyConstantValues() {
-        assertEquals(TimeUnit.DAYS.toMillis(1),        StandardDateFormat.MILLISECONDS_PER_DAY);
-        assertEquals(TimeUnit.MILLISECONDS.toNanos(1), StandardDateFormat.NANOS_PER_MILLISECOND);
-        assertEquals(TimeUnit.SECONDS.toNanos(1),      StandardDateFormat.NANOS_PER_SECOND);
-    }
-
-    /**
      * Tests {@link StandardDateFormat#toISO(CharSequence, int, int)}.
      */
     @Test
@@ -139,9 +127,9 @@
      */
     @Test
     public void testNegativeYear() throws ParseException {
-        final Date julian = new Date(-210866760000000L);            // Same epoch as CommonCRS.Temporal.JULIAN.
-        final String expected = "-4713-11-24T12:00:00.000";         // Proleptic Gregorian calendar, astronomical year.
-        final StandardDateFormat f = new StandardDateFormat();
+        final var julian = new Date(-210866760000000L);         // Same epoch as CommonCRS.Temporal.JULIAN.
+        final var expected = "-4713-11-24T12:00:00.000";        // Proleptic Gregorian calendar, astronomical year.
+        final var f = new StandardDateFormat();
         assertEquals(expected, f.format(julian));
         assertEquals(julian, f.parse(expected));
     }
diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
index 28491f1..fce63c7 100644
--- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
+++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
@@ -89,7 +89,7 @@
 import org.apache.sis.portrayal.RenderException;
 import org.apache.sis.portrayal.TransformChangeEvent;
 import static org.apache.sis.gui.internal.LogHandler.LOGGER;
-import static org.apache.sis.util.privy.StandardDateFormat.NANOS_PER_MILLISECOND;
+import static org.apache.sis.util.privy.Constants.NANOS_PER_MILLISECOND;
 
 // Specific to the main branch:
 import org.opengis.geometry.MismatchedDimensionException;
diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/metadata/IdentificationInfo.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/metadata/IdentificationInfo.java
index e5617a1..7a32422 100644
--- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/metadata/IdentificationInfo.java
+++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/metadata/IdentificationInfo.java
@@ -21,6 +21,7 @@
 import java.util.Collection;
 import java.util.LinkedHashSet;
 import java.util.StringJoiner;
+import java.time.Instant;
 import javafx.concurrent.Task;
 import javafx.geometry.HPos;
 import javafx.scene.canvas.Canvas;
@@ -362,7 +363,7 @@
          */
         text = null;
         Identifier identifier = null;
-        Range<Date> timeRange = null;
+        Range<Instant> timeRange = null;
         Range<Double> heights = null;
         for (final Extent extent : nonNull(dataInfo != null ? dataInfo.getExtents() : null)) {
             if (extent != null) {
@@ -379,7 +380,7 @@
                 }
                 final MeasurementRange<Double> v = Extents.getVerticalRange(extent);
                 if (v != null) heights = (heights != null) ? heights.union(v) : v;
-                final Range<Date> t = Extents.getTimeRange(extent);
+                final Range<Instant> t = Extents.getTimeRange(extent, null).orElse(null);
                 if (t != null) timeRange = (timeRange != null) ? timeRange.union(t) : t;
             }
         }
@@ -398,12 +399,12 @@
         addLine(Vocabulary.Keys.Extent, text);
         if (timeRange != null) {
             label = Vocabulary.Keys.StartDate;
-            Date t = timeRange.getMinValue();
+            Instant t = timeRange.getMinValue();
             if (t == null) {
                 t = timeRange.getMaxValue();
                 label = Vocabulary.Keys.EndDate;
             }
-            addLine(label, owner.format(t));
+            addLine(label, owner.format(Date.from(t)));
         }
         if (heights != null) {
             final Double min = heights.getMinValue();
diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/AuthorityCodes.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/AuthorityCodes.java
index b9e523c..45565a8 100644
--- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/AuthorityCodes.java
+++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/AuthorityCodes.java
@@ -41,7 +41,7 @@
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.collection.BackingStoreException;
-import org.apache.sis.util.privy.StandardDateFormat;
+import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.Strings;
 import org.apache.sis.gui.internal.BackgroundThreads;
 import static org.apache.sis.gui.internal.LogHandler.LOGGER;
@@ -67,7 +67,7 @@
      * The delay value is a compromise between fast user experience and giving enough time for doing a few
      * large data transfers instead of many small data transfers.
      */
-    private static final long REFRESH_DELAY = StandardDateFormat.NANOS_PER_SECOND / 10;
+    private static final long REFRESH_DELAY = Constants.NANOS_PER_SECOND / 10;
 
     /**
      * The table view which use this list, or {@code null} if we don't need this information anymore.
@@ -447,7 +447,7 @@
                  * the `toDescribe` list to be populated with more requests, then process them.
                  */
                 if (codes.isEmpty()) {
-                    Thread.sleep(REFRESH_DELAY / StandardDateFormat.NANOS_PER_MILLISECOND);
+                    Thread.sleep(REFRESH_DELAY / Constants.NANOS_PER_MILLISECOND);
                     return new PartialResult(null, processNameRequests(factory));
                 }
             } catch (BackingStoreException e) {